(Feature) - V2 fetch supported fiat currencies from client gateway (#2023)

* Replace collectibles fetch with client gateway

* Updates tokenProps types

* Replaces balance endpoint with client gateway

* Remove default export of tokens list

* Set the default rows per page to 100

* Fix ether price load

* Remove Add custom token button

* Remove add custom asset and add custom token modals

* Remove default exports

* Remove currencyValues state from store
Remove currencyValues selectors

* Update balance state with fiatBalance and tokenBalance

* Remove default export from fetchEtherBalance.ts

* Fix safeFiatBalancesTotalSelector
Add totalFiatBalance to safe store

* Remove fetchCurrenciesRates.ts

* Adds in currencyValuesStorageMiddleware logic for updating the safe tokens when the user changes the selected currency

* Move selectedCurrency to simple redux state
Remove currencyValues redux state

* Updates fetchTokenCurrenciesBalances with selectedCurrency parameter

* Revert CurrencyValuesState
Remove selectedCurrency from safe state

* Remove selectedCurrency from safe state
Update currentCurrencySelector selector usage

* Add fetchAvailableCurrencies setAvailableCurrencies and updateAvailableCurrencies

* Remove availableCurrencies.ts by using availableCurrenciesSelector

* Fix display of ETH balance on extendedSafeTokensSelector and extractDataFromRESULT

* Fix multiple calls to token balance endpoint

* (Feature) - V2 Remove Manage List (#2032)

Co-authored-by: fernandomg <fernando.greco@gmail.com>
This commit is contained in:
Agustin Pane 2021-03-17 14:28:37 -03:00 committed by GitHub
parent 1c74f39f37
commit 4ed181886b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
66 changed files with 236 additions and 1581 deletions

View File

@ -20,13 +20,17 @@ import { getNetworkId } from 'src/config'
import { ETHEREUM_NETWORK } from 'src/config/networks/network.d'
import { networkSelector } from 'src/logic/wallets/store/selectors'
import { SAFELIST_ADDRESS, WELCOME_ADDRESS } from 'src/routes/routes'
import { safeNameSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
import {
safeFiatBalancesTotalSelector,
safeNameSelector,
safeParamAddressFromStateSelector,
} from 'src/logic/safe/store/selectors'
import { currentCurrencySelector } from 'src/logic/currencyValues/store/selectors'
import Modal from 'src/components/Modal'
import SendModal from 'src/routes/safe/components/Balances/SendModal'
import { useLoadSafe } from 'src/logic/safe/hooks/useLoadSafe'
import { useSafeScheduledUpdates } from 'src/logic/safe/hooks/useSafeScheduledUpdates'
import useSafeActions from 'src/logic/safe/hooks/useSafeActions'
import { currentCurrencySelector, safeFiatBalancesTotalSelector } from 'src/logic/currencyValues/store/selectors'
import { formatAmountInUsFormat } from 'src/logic/tokens/utils/formatAmount'
import { grantedSelector } from 'src/routes/safe/container/selector'

View File

@ -6,7 +6,6 @@ import { Integrations } from '@sentry/tracing'
import Root from 'src/components/Root'
import loadCurrentSessionFromStorage from 'src/logic/currentSession/store/actions/loadCurrentSessionFromStorage'
import loadActiveTokens from 'src/logic/tokens/store/actions/loadActiveTokens'
import loadDefaultSafe from 'src/logic/safe/store/actions/loadDefaultSafe'
import loadSafesFromStorage from 'src/logic/safe/store/actions/loadSafesFromStorage'
import { store } from 'src/store'
@ -17,7 +16,6 @@ disableMMAutoRefreshWarning()
BigNumber.set({ EXPONENTIAL_AT: [-7, 255] })
store.dispatch(loadActiveTokens())
store.dispatch(loadSafesFromStorage())
store.dispatch(loadDefaultSafe())
store.dispatch(loadCurrentSessionFromStorage())

View File

@ -3,8 +3,6 @@ import { NFTAsset, NFTAssets, NFTToken, NFTTokens } from 'src/logic/collectibles
import { AppReduxState } from 'src/store'
import { NFT_ASSETS_REDUCER_ID, NFT_TOKENS_REDUCER_ID } from 'src/logic/collectibles/store/reducer/collectibles'
import { safeActiveAssetsSelector } from 'src/logic/safe/store/selectors'
export const nftAssets = (state: AppReduxState): NFTAssets => state[NFT_ASSETS_REDUCER_ID]
export const nftTokens = (state: AppReduxState): NFTTokens => state[NFT_TOKENS_REDUCER_ID]
@ -26,21 +24,8 @@ export const orderedNFTAssets = createSelector(nftTokensSelector, (userNftTokens
export const activeNftAssetsListSelector = createSelector(
nftAssetsListSelector,
safeActiveAssetsSelector,
availableNftAssetsAddresses,
(assets, activeAssetsList, availableNftAssetsAddresses): NFTAsset[] => {
return assets
.filter(({ address }) => activeAssetsList.has(address))
.filter(({ address }) => availableNftAssetsAddresses.includes(address))
},
)
export const safeActiveSelectorMap = createSelector(
activeNftAssetsListSelector,
(activeAssets): NFTAssets => {
return activeAssets.reduce((acc, asset) => {
acc[asset.address] = asset
return acc
}, {})
(assets, availableNftAssetsAddresses): NFTAsset[] => {
return assets.filter(({ address }) => availableNftAssetsAddresses.includes(address))
},
)

View File

@ -0,0 +1,8 @@
import { getClientGatewayUrl } from 'src/config'
import axios from 'axios'
export const fetchAvailableCurrencies = async (): Promise<string[]> => {
const url = `${getClientGatewayUrl()}/balances/supported-fiat-codes`
return axios.get(url).then(({ data }) => data)
}

View File

@ -1,41 +0,0 @@
import axios from 'axios'
import BigNumber from 'bignumber.js'
import { EXCHANGE_RATE_URL } from 'src/utils/constants'
import { fetchTokenCurrenciesBalances } from './fetchTokenCurrenciesBalances'
import { sameString } from 'src/utils/strings'
import { AVAILABLE_CURRENCIES } from 'src/logic/currencyValues/store/model/currencyValues'
const fetchCurrenciesRates = async (
baseCurrency: string,
targetCurrencyValue: string,
safeAddress: string,
): Promise<number> => {
let rate = 0
if (sameString(targetCurrencyValue, AVAILABLE_CURRENCIES.NETWORK)) {
try {
const tokenCurrenciesBalances = await fetchTokenCurrenciesBalances(safeAddress)
if (tokenCurrenciesBalances.items.length) {
rate = new BigNumber(1).div(tokenCurrenciesBalances.items[0].fiatConversion).toNumber()
}
} catch (error) {
console.error(`Fetching ${AVAILABLE_CURRENCIES.NETWORK} data from the relayer errored`, error)
}
return rate
}
// National currencies
try {
const url = `${EXCHANGE_RATE_URL}?base=${baseCurrency}&symbols=${targetCurrencyValue}`
const result = await axios.get(url)
if (result?.data) {
const { rates } = result.data
rate = rates[targetCurrencyValue] ? rates[targetCurrencyValue] : 0
}
} catch (error) {
console.error('Fetching data from getExchangeRatesUrl errored', error)
}
return rate
}
export default fetchCurrenciesRates

View File

@ -1,27 +0,0 @@
import { Action } from 'redux-actions'
import { ThunkDispatch } from 'redux-thunk'
import fetchCurrenciesRates from 'src/logic/currencyValues/api/fetchCurrenciesRates'
import { setCurrencyRate } from 'src/logic/currencyValues/store/actions/setCurrencyRate'
import { AVAILABLE_CURRENCIES } from 'src/logic/currencyValues/store/model/currencyValues'
import { CurrencyRatePayload } from 'src/logic/currencyValues/store/reducer/currencyValues'
import { AppReduxState } from 'src/store'
const fetchCurrencyRate = (safeAddress: string, selectedCurrency: string) => async (
dispatch: ThunkDispatch<AppReduxState, undefined, Action<CurrencyRatePayload>>,
): Promise<void> => {
if (AVAILABLE_CURRENCIES.USD === selectedCurrency) {
dispatch(setCurrencyRate(safeAddress, 1))
return
}
const selectedCurrencyRateInBaseCurrency: number = await fetchCurrenciesRates(
AVAILABLE_CURRENCIES.USD,
selectedCurrency,
safeAddress,
)
dispatch(setCurrencyRate(safeAddress, selectedCurrencyRateInBaseCurrency))
}
export default fetchCurrencyRate

View File

@ -2,18 +2,17 @@ import { Action } from 'redux-actions'
import { ThunkDispatch } from 'redux-thunk'
import { setSelectedCurrency } from 'src/logic/currencyValues/store/actions/setSelectedCurrency'
import { AVAILABLE_CURRENCIES } from 'src/logic/currencyValues/store/model/currencyValues'
import { CurrentCurrencyPayload } from 'src/logic/currencyValues/store/reducer/currencyValues'
import { loadSelectedCurrency } from 'src/logic/currencyValues/store/utils/currencyValuesStorage'
import { AppReduxState } from 'src/store'
export const fetchSelectedCurrency = (safeAddress: string) => async (
dispatch: ThunkDispatch<AppReduxState, undefined, Action<CurrentCurrencyPayload>>,
import { loadSelectedCurrency } from 'src/logic/safe/utils/currencyValuesStorage'
import { AppReduxState } from 'src/store'
import { SelectedCurrencyPayload } from 'src/logic/currencyValues/store/reducer/currencyValues'
export const fetchSelectedCurrency = () => async (
dispatch: ThunkDispatch<AppReduxState, undefined, Action<SelectedCurrencyPayload>>,
): Promise<void> => {
try {
const storedSelectedCurrency = await loadSelectedCurrency()
dispatch(setSelectedCurrency(safeAddress, storedSelectedCurrency || AVAILABLE_CURRENCIES.USD))
dispatch(setSelectedCurrency({ selectedCurrency: storedSelectedCurrency || 'USD' }))
} catch (err) {
console.error('Error fetching currency values', err)
}

View File

@ -0,0 +1,6 @@
import { createAction } from 'redux-actions'
import { AvailableCurrenciesPayload } from 'src/logic/currencyValues/store/reducer/currencyValues'
export const SET_AVAILABLE_CURRENCIES = 'SET_AVAILABLE_CURRENCIES'
export const setAvailableCurrencies = createAction<AvailableCurrenciesPayload>(SET_AVAILABLE_CURRENCIES)

View File

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

View File

@ -1,9 +0,0 @@
import { createAction } from 'redux-actions'
export const SET_CURRENCY_RATE = 'SET_CURRENCY_RATE'
// eslint-disable-next-line max-len
export const setCurrencyRate = createAction(SET_CURRENCY_RATE, (safeAddress: string, currencyRate: number) => ({
safeAddress,
currencyRate,
}))

View File

@ -1,20 +1,6 @@
import { Action, createAction } from 'redux-actions'
import { ThunkDispatch } from 'redux-thunk'
import { CurrencyPayloads } from 'src/logic/currencyValues/store/reducer/currencyValues'
import { AppReduxState } from 'src/store'
import fetchCurrencyRate from 'src/logic/currencyValues/store/actions/fetchCurrencyRate'
import { createAction } from 'redux-actions'
import { SelectedCurrencyPayload } from 'src/logic/currencyValues/store/reducer/currencyValues'
export const SET_CURRENT_CURRENCY = 'SET_CURRENT_CURRENCY'
const setCurrentCurrency = createAction(SET_CURRENT_CURRENCY, (safeAddress: string, selectedCurrency: string) => ({
safeAddress,
selectedCurrency,
}))
export const setSelectedCurrency = (safeAddress: string, selectedCurrency: string) => (
dispatch: ThunkDispatch<AppReduxState, undefined, Action<CurrencyPayloads>>,
): void => {
dispatch(setCurrentCurrency(safeAddress, selectedCurrency))
dispatch(fetchCurrencyRate(safeAddress, selectedCurrency))
}
export const setSelectedCurrency = createAction<SelectedCurrencyPayload>(SET_CURRENT_CURRENCY)

View File

@ -0,0 +1,18 @@
import { Action } from 'redux-actions'
import { ThunkDispatch } from 'redux-thunk'
import { AppReduxState } from 'src/store'
import { AvailableCurrenciesPayload } from 'src/logic/currencyValues/store/reducer/currencyValues'
import { setAvailableCurrencies } from 'src/logic/currencyValues/store/actions/setAvailableCurrencies'
import { fetchAvailableCurrencies } from 'src/logic/currencyValues/api/fetchAvailableCurrencies'
export const updateAvailableCurrencies = () => async (
dispatch: ThunkDispatch<AppReduxState, undefined, Action<AvailableCurrenciesPayload>>,
): Promise<void> => {
try {
const availableCurrencies = await fetchAvailableCurrencies()
dispatch(setAvailableCurrencies({ availableCurrencies }))
} catch (err) {
console.error('Error fetching available currencies', err)
}
return Promise.resolve()
}

View File

@ -1,16 +1,15 @@
import { SET_CURRENT_CURRENCY } from 'src/logic/currencyValues/store/actions/setSelectedCurrency'
import { saveSelectedCurrency } from 'src/logic/currencyValues/store/utils/currencyValuesStorage'
import { saveSelectedCurrency } from 'src/logic/safe/utils/currencyValuesStorage'
const watchedActions = [SET_CURRENT_CURRENCY]
const currencyValuesStorageMiddleware = () => (next) => async (action) => {
export const currencyValuesStorageMiddleware = () => (next) => async (action) => {
const handledAction = next(action)
if (watchedActions.includes(action.type)) {
switch (action.type) {
case SET_CURRENT_CURRENCY: {
const { selectedCurrency } = action.payload
saveSelectedCurrency(selectedCurrency)
await saveSelectedCurrency(selectedCurrency)
break
}
@ -21,5 +20,3 @@ const currencyValuesStorageMiddleware = () => (next) => async (action) => {
return handledAction
}
export default currencyValuesStorageMiddleware

View File

@ -1,66 +0,0 @@
import { List, Record, RecordOf } from 'immutable'
import { getNetworkInfo } from 'src/config'
const { nativeCoin } = getNetworkInfo()
export const AVAILABLE_CURRENCIES = {
NETWORK: nativeCoin.symbol.toLocaleUpperCase(),
USD: 'USD',
EUR: 'EUR',
AUD: 'AUD',
BGN: 'BGN',
BRL: 'BRL',
CAD: 'CAD',
CHF: 'CHF',
CNY: 'CNY',
CZK: 'CZK',
DKK: 'DKK',
GBP: 'GBP',
HKD: 'HKD',
HRK: 'HRK',
HUF: 'HUF',
IDR: 'IDR',
ILS: 'ILS',
INR: 'INR',
ISK: 'ISK',
JPY: 'JPY',
KRW: 'KRW',
MXN: 'MXN',
MYR: 'MYR',
NOK: 'NOK',
NZD: 'NZD',
PHP: 'PHP',
PLN: 'PLN',
RON: 'RON',
RUB: 'RUB',
SEK: 'SEK',
SGD: 'SGD',
THB: 'THB',
TRY: 'TRY',
ZAR: 'ZAR',
} as const
export type BalanceCurrencyRecord = {
currencyName?: string
tokenAddress?: string
balanceInBaseCurrency: string
balanceInSelectedCurrency: string
}
export const makeBalanceCurrency = Record<BalanceCurrencyRecord>({
currencyName: '',
tokenAddress: '',
balanceInBaseCurrency: '',
balanceInSelectedCurrency: '',
})
export type CurrencyRateValueRecord = RecordOf<BalanceCurrencyRecord>
export type BalanceCurrencyList = List<CurrencyRateValueRecord>
export interface CurrencyRateValue {
currencyRate?: number
selectedCurrency?: string
currencyBalances?: BalanceCurrencyList
}

View File

@ -1,44 +1,35 @@
import { Map } from 'immutable'
import { Action, handleActions } from 'redux-actions'
import { SET_CURRENCY_BALANCES } from 'src/logic/currencyValues/store/actions/setCurrencyBalances'
import { SET_CURRENCY_RATE } from 'src/logic/currencyValues/store/actions/setCurrencyRate'
import { SET_CURRENT_CURRENCY } from 'src/logic/currencyValues/store/actions/setSelectedCurrency'
import { BalanceCurrencyList, CurrencyRateValue } from 'src/logic/currencyValues/store/model/currencyValues'
import { AppReduxState } from 'src/store'
import { SET_AVAILABLE_CURRENCIES } from 'src/logic/currencyValues/store/actions/setAvailableCurrencies'
export const CURRENCY_VALUES_KEY = 'currencyValues'
export interface CurrencyReducerMap extends Map<string, any> {
get<K extends keyof CurrencyRateValue>(key: K, notSetValue?: unknown): CurrencyRateValue[K]
setIn<K extends keyof CurrencyRateValue>(keys: [string, K], value: CurrencyRateValue[K]): this
export type CurrencyValuesState = {
selectedCurrency: string
availableCurrencies: string[]
}
export type CurrencyValuesState = Map<string, CurrencyReducerMap>
export const initialState = {
selectedCurrency: 'USD',
availableCurrencies: ['USD'],
}
type CurrencyBasePayload = { safeAddress: string }
export type CurrencyRatePayload = CurrencyBasePayload & { currencyRate: number }
export type CurrencyBalancesPayload = CurrencyBasePayload & { currencyBalances: BalanceCurrencyList }
export type CurrentCurrencyPayload = CurrencyBasePayload & { selectedCurrency: string }
export type SelectedCurrencyPayload = { selectedCurrency: string }
export type AvailableCurrenciesPayload = { availableCurrencies: string[] }
export type CurrencyPayloads = CurrencyRatePayload | CurrencyBalancesPayload | CurrentCurrencyPayload
export default handleActions<CurrencyReducerMap, CurrencyPayloads>(
export default handleActions<AppReduxState['currencyValues'], CurrencyValuesState>(
{
[SET_CURRENCY_RATE]: (state, action: Action<CurrencyRatePayload>) => {
const { currencyRate, safeAddress } = action.payload
return state.setIn([safeAddress, 'currencyRate'], currencyRate)
[SET_CURRENT_CURRENCY]: (state, action: Action<SelectedCurrencyPayload>) => {
const { selectedCurrency } = action.payload
state.selectedCurrency = selectedCurrency
return state
},
[SET_CURRENCY_BALANCES]: (state, action: Action<CurrencyBalancesPayload>) => {
const { safeAddress, currencyBalances } = action.payload
return state.setIn([safeAddress, 'currencyBalances'], currencyBalances)
},
[SET_CURRENT_CURRENCY]: (state, action: Action<CurrentCurrencyPayload>) => {
const { safeAddress, selectedCurrency } = action.payload
return state.setIn([safeAddress, 'selectedCurrency'], selectedCurrency)
[SET_AVAILABLE_CURRENCIES]: (state, action: Action<AvailableCurrenciesPayload>) => {
const { availableCurrencies } = action.payload
state.availableCurrencies = availableCurrencies
return state
},
},
Map(),
initialState,
)

View File

@ -1,53 +1,12 @@
import { createSelector } from 'reselect'
import {
CURRENCY_VALUES_KEY,
CurrencyReducerMap,
CurrencyValuesState,
} from 'src/logic/currencyValues/store/reducer/currencyValues'
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
import { AppReduxState } from 'src/store'
import { CurrencyRateValue } from 'src/logic/currencyValues/store/model/currencyValues'
import { BigNumber } from 'bignumber.js'
import { CURRENCY_VALUES_KEY, CurrencyValuesState } from 'src/logic/currencyValues/store/reducer/currencyValues'
export const currencyValuesSelector = (state: AppReduxState): CurrencyValuesState => state[CURRENCY_VALUES_KEY]
export const safeFiatBalancesSelector = createSelector(
currencyValuesSelector,
safeParamAddressFromStateSelector,
(currencyValues, safeAddress): CurrencyReducerMap | undefined => {
if (!currencyValues || !safeAddress) return
return currencyValues.get(safeAddress)
},
)
export const currentCurrencySelector = (state: AppReduxState): string => {
return state[CURRENCY_VALUES_KEY].selectedCurrency
}
const currencyValueSelector = <K extends keyof CurrencyRateValue>(key: K) => (
currencyValuesMap?: CurrencyReducerMap,
): CurrencyRateValue[K] => currencyValuesMap?.get(key)
export const safeFiatBalancesListSelector = createSelector(
safeFiatBalancesSelector,
currencyValueSelector('currencyBalances'),
)
export const currentCurrencySelector = createSelector(
safeFiatBalancesSelector,
currencyValueSelector('selectedCurrency'),
)
export const currencyRateSelector = createSelector(safeFiatBalancesSelector, currencyValueSelector('currencyRate'))
export const safeFiatBalancesTotalSelector = createSelector(
safeFiatBalancesListSelector,
currencyRateSelector,
(currencyBalances, currencyRate): string | null => {
if (!currencyBalances) return '0'
if (!currencyRate) return null
const totalInBaseCurrency = currencyBalances.reduce((total, balanceCurrencyRecord) => {
return total.plus(balanceCurrencyRecord.balanceInBaseCurrency)
}, new BigNumber(0))
return totalInBaseCurrency.times(currencyRate).toFixed(2)
},
)
export const availableCurrenciesSelector = (state: AppReduxState): string[] => {
return state[CURRENCY_VALUES_KEY].availableCurrencies
}

View File

@ -1,10 +1,7 @@
import axios from 'axios'
import { getSafeClientGatewayBaseUrl } from 'src/config'
import {
fetchTokenCurrenciesBalances,
BalanceEndpoint,
} from 'src/logic/currencyValues/api/fetchTokenCurrenciesBalances'
import { fetchTokenCurrenciesBalances } from 'src/logic/safe/api/fetchTokenCurrenciesBalances'
import { aNewStore } from 'src/store'
jest.mock('axios')
@ -52,11 +49,15 @@ describe('fetchTokenCurrenciesBalances', () => {
axios.get.mockImplementationOnce(() => Promise.resolve({ data: expectedResult }))
// when
const result = await fetchTokenCurrenciesBalances(safeAddress, excludeSpamTokens)
const result = await fetchTokenCurrenciesBalances({
safeAddress,
excludeSpamTokens,
selectedCurrency: 'USD',
})
// then
expect(result).toStrictEqual(expectedResult)
expect(axios.get).toHaveBeenCalled()
expect(axios.get).toBeCalledWith(`${apiUrl}/balances/usd/?trusted=false&exclude_spam=${excludeSpamTokens}`)
expect(axios.get).toBeCalledWith(`${apiUrl}/balances/USD/?trusted=false&exclude_spam=${excludeSpamTokens}`)
})
})

View File

@ -16,14 +16,22 @@ export type BalanceEndpoint = {
items: TokenBalance[]
}
export const fetchTokenCurrenciesBalances = (
safeAddress: string,
type FetchTokenCurrenciesBalancesProps = {
safeAddress: string
selectedCurrency: string
excludeSpamTokens?: boolean
trustedTokens?: boolean
}
export const fetchTokenCurrenciesBalances = async ({
safeAddress,
selectedCurrency,
excludeSpamTokens = true,
trustedTokens = false,
): Promise<BalanceEndpoint> => {
}: FetchTokenCurrenciesBalancesProps): Promise<BalanceEndpoint> => {
const url = `${getSafeClientGatewayBaseUrl(
checksumAddress(safeAddress),
)}/balances/usd/?trusted=${trustedTokens}&exclude_spam=${excludeSpamTokens}`
)}/balances/${selectedCurrency}/?trusted=${trustedTokens}&exclude_spam=${excludeSpamTokens}`
return axios.get(url).then(({ data }) => data)
}

View File

@ -1,35 +1,32 @@
import { useMemo } from 'react'
import { batch, useDispatch } from 'react-redux'
import { batch, useDispatch, useSelector } from 'react-redux'
import { useLocation } from 'react-router-dom'
import { fetchCollectibles } from 'src/logic/collectibles/store/actions/fetchCollectibles'
import { fetchSelectedCurrency } from 'src/logic/currencyValues/store/actions/fetchSelectedCurrency'
import activateAssetsByBalance from 'src/logic/tokens/store/actions/activateAssetsByBalance'
import fetchSafeTokens from 'src/logic/tokens/store/actions/fetchSafeTokens'
import { fetchSafeTokens } from 'src/logic/tokens/store/actions/fetchSafeTokens'
import { fetchTokens } from 'src/logic/tokens/store/actions/fetchTokens'
import { COINS_LOCATION_REGEX, COLLECTIBLES_LOCATION_REGEX } from 'src/routes/safe/components/Balances'
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
import { currentCurrencySelector } from 'src/logic/currencyValues/store/selectors'
export const useFetchTokens = (safeAddress: string): void => {
const dispatch = useDispatch<Dispatch>()
const location = useLocation()
const currentCurrency = useSelector(currentCurrencySelector)
useMemo(() => {
if (COINS_LOCATION_REGEX.test(location.pathname)) {
batch(() => {
// fetch tokens there to get symbols for tokens in TXs list
dispatch(fetchTokens())
dispatch(fetchSelectedCurrency(safeAddress))
dispatch(fetchSafeTokens(safeAddress))
dispatch(fetchSelectedCurrency())
dispatch(fetchSafeTokens(safeAddress, currentCurrency))
})
}
if (COLLECTIBLES_LOCATION_REGEX.test(location.pathname)) {
batch(() => {
dispatch(fetchCollectibles(safeAddress)).then(() => {
dispatch(activateAssetsByBalance(safeAddress))
})
})
dispatch(fetchCollectibles(safeAddress))
}
}, [dispatch, location.pathname, safeAddress])
}, [dispatch, location.pathname, safeAddress, currentCurrency])
}

View File

@ -3,11 +3,12 @@ import { useDispatch } from 'react-redux'
import loadAddressBookFromStorage from 'src/logic/addressBook/store/actions/loadAddressBookFromStorage'
import addViewedSafe from 'src/logic/currentSession/store/actions/addViewedSafe'
import fetchSafeTokens from 'src/logic/tokens/store/actions/fetchSafeTokens'
import { fetchSafeTokens } from 'src/logic/tokens/store/actions/fetchSafeTokens'
import fetchLatestMasterContractVersion from 'src/logic/safe/store/actions/fetchLatestMasterContractVersion'
import fetchSafe from 'src/logic/safe/store/actions/fetchSafe'
import fetchTransactions from 'src/logic/safe/store/actions/transactions/fetchTransactions'
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
import { updateAvailableCurrencies } from 'src/logic/currencyValues/store/actions/updateAvailableCurrencies'
export const useLoadSafe = (safeAddress?: string): boolean => {
const dispatch = useDispatch<Dispatch>()
@ -20,6 +21,7 @@ export const useLoadSafe = (safeAddress?: string): boolean => {
await dispatch(fetchSafe(safeAddress))
setIsSafeLoaded(true)
await dispatch(fetchSafeTokens(safeAddress))
dispatch(updateAvailableCurrencies())
dispatch(fetchTransactions(safeAddress))
dispatch(addViewedSafe(safeAddress))
}

View File

@ -2,8 +2,8 @@ import { useEffect, useRef } from 'react'
import { batch, useDispatch } from 'react-redux'
import { fetchCollectibles } from 'src/logic/collectibles/store/actions/fetchCollectibles'
import fetchSafeTokens from 'src/logic/tokens/store/actions/fetchSafeTokens'
import fetchEtherBalance from 'src/logic/safe/store/actions/fetchEtherBalance'
import { fetchSafeTokens } from 'src/logic/tokens/store/actions/fetchSafeTokens'
import { fetchEtherBalance } from 'src/logic/safe/store/actions/fetchEtherBalance'
import { checkAndUpdateSafe } from 'src/logic/safe/store/actions/fetchSafe'
import fetchTransactions from 'src/logic/safe/store/actions/transactions/fetchTransactions'
import { TIMEOUT } from 'src/utils/constants'

View File

@ -1,7 +0,0 @@
import { createAction } from 'redux-actions'
export const ACTIVATE_TOKEN_FOR_ALL_SAFES = 'ACTIVATE_TOKEN_FOR_ALL_SAFES'
const activateTokenForAllSafes = createAction(ACTIVATE_TOKEN_FOR_ALL_SAFES)
export default activateTokenForAllSafes

View File

@ -5,7 +5,7 @@ import { Dispatch } from 'redux'
import { backOff } from 'exponential-backoff'
import { AppReduxState } from 'src/store'
const fetchEtherBalance = (safeAddress: string) => async (
export const fetchEtherBalance = (safeAddress: string) => async (
dispatch: Dispatch,
getState: () => AppReduxState,
): Promise<void> => {
@ -21,5 +21,3 @@ const fetchEtherBalance = (safeAddress: string) => async (
console.error('Error when fetching Ether balance:', err)
}
}
export default fetchEtherBalance

View File

@ -80,6 +80,7 @@ export const buildSafe = async (
threshold,
owners,
ethBalance,
totalFiatBalance: 0,
nonce,
currentVersion: currentVersion ?? '',
needsUpdate,
@ -88,8 +89,6 @@ export const buildSafe = async (
latestIncomingTxBlock: 0,
activeAssets: Set(),
activeTokens: Set(),
blacklistedAssets: Set(),
blacklistedTokens: Set(),
modules,
spendingLimits,
}

View File

@ -1,9 +0,0 @@
import { Set } from 'immutable'
import updateAssetsList from './updateAssetsList'
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
const updateActiveAssets = (safeAddress: string, activeAssets: Set<string>) => (dispatch: Dispatch): void => {
dispatch(updateAssetsList({ safeAddress, activeAssets }))
}
export default updateActiveAssets

View File

@ -1,19 +0,0 @@
import { Set } from 'immutable'
import updateTokensList from './updateTokensList'
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
// 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
// const generateMatchProps = (safeAddress: string) => ({
// match: {
// params: {
// [SAFE_PARAM_ADDRESS]: safeAddress,
// },
// },
// })
const updateActiveTokens = (safeAddress: string, activeTokens: Set<string>) => (dispatch: Dispatch): void => {
dispatch(updateTokensList({ safeAddress, activeTokens }))
}
export default updateActiveTokens

View File

@ -1,7 +0,0 @@
import { createAction } from 'redux-actions'
export const UPDATE_ASSETS_LIST = 'UPDATE_ASSETS_LIST'
const updateAssetsList = createAction(UPDATE_ASSETS_LIST)
export default updateAssetsList

View File

@ -1,9 +0,0 @@
import { Set } from 'immutable'
import updateAssetsList from './updateAssetsList'
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
const updateBlacklistedAssets = (safeAddress: string, blacklistedAssets: Set<string>) => (dispatch: Dispatch): void => {
dispatch(updateAssetsList({ safeAddress, blacklistedAssets }))
}
export default updateBlacklistedAssets

View File

@ -1,9 +0,0 @@
import { Set } from 'immutable'
import updateTokensList from './updateTokensList'
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
const updateBlacklistedTokens = (safeAddress: string, blacklistedTokens: Set<string>) => (dispatch: Dispatch): void => {
dispatch(updateTokensList({ safeAddress, blacklistedTokens }))
}
export default updateBlacklistedTokens

View File

@ -1,7 +0,0 @@
import { createAction } from 'redux-actions'
export const UPDATE_TOKENS_LIST = 'UPDATE_TOKENS_LIST'
const updateTokenList = createAction(UPDATE_TOKENS_LIST)
export default updateTokenList

View File

@ -1,7 +1,4 @@
import { saveDefaultSafe, saveSafes } from 'src/logic/safe/utils'
import { tokensSelector } from 'src/logic/tokens/store/selectors'
import { saveActiveTokens } from 'src/logic/tokens/utils/tokensStorage'
import { ACTIVATE_TOKEN_FOR_ALL_SAFES } from 'src/logic/safe/store/actions/activateTokenForAllSafes'
import { ADD_SAFE_OWNER } from 'src/logic/safe/store/actions/addSafeOwner'
import { EDIT_SAFE_OWNER } from 'src/logic/safe/store/actions/editSafeOwner'
import { REMOVE_SAFE } from 'src/logic/safe/store/actions/removeSafe'
@ -9,9 +6,7 @@ import { REMOVE_SAFE_OWNER } from 'src/logic/safe/store/actions/removeSafeOwner'
import { REPLACE_SAFE_OWNER } from 'src/logic/safe/store/actions/replaceSafeOwner'
import { SET_DEFAULT_SAFE } from 'src/logic/safe/store/actions/setDefaultSafe'
import { UPDATE_SAFE } from 'src/logic/safe/store/actions/updateSafe'
import { UPDATE_TOKENS_LIST } from 'src/logic/safe/store/actions/updateTokensList'
import { UPDATE_ASSETS_LIST } from 'src/logic/safe/store/actions/updateAssetsList'
import { getActiveTokensAddressesForAllSafes, safesMapSelector } from 'src/logic/safe/store/selectors'
import { safesMapSelector } from 'src/logic/safe/store/selectors'
import { ADD_OR_UPDATE_SAFE } from 'src/logic/safe/store/actions/addOrUpdateSafe'
import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
import { checksumAddress } from 'src/utils/checksumAddress'
@ -26,28 +21,10 @@ const watchedActions = [
REMOVE_SAFE_OWNER,
REPLACE_SAFE_OWNER,
EDIT_SAFE_OWNER,
ACTIVATE_TOKEN_FOR_ALL_SAFES,
UPDATE_TOKENS_LIST,
UPDATE_ASSETS_LIST,
SET_DEFAULT_SAFE,
]
const recalculateActiveTokens = (state) => {
const tokens = tokensSelector(state)
const activeTokenAddresses = getActiveTokensAddressesForAllSafes(state)
const activeTokens = tokens.withMutations((map) => {
map.forEach((token) => {
if (!activeTokenAddresses.has(token.address)) {
map.remove(token.address)
}
})
})
saveActiveTokens(activeTokens)
}
const safeStorageMware = (store) => (next) => async (action) => {
export const safeStorageMiddleware = (store) => (next) => async (action) => {
const handledAction = next(action)
if (watchedActions.includes(action.type)) {
@ -57,10 +34,6 @@ const safeStorageMware = (store) => (next) => async (action) => {
await saveSafes(safes.toJSON())
switch (action.type) {
case ACTIVATE_TOKEN_FOR_ALL_SAFES: {
recalculateActiveTokens(state)
break
}
case ADD_OR_UPDATE_SAFE: {
const { safe } = action.payload
safe.owners.forEach((owner) => {
@ -72,10 +45,7 @@ const safeStorageMware = (store) => (next) => async (action) => {
break
}
case UPDATE_SAFE: {
const { activeTokens, name, address } = action.payload
if (activeTokens) {
recalculateActiveTokens(state)
}
const { name, address } = action.payload
if (name) {
dispatch(addOrUpdateAddressBookEntry(makeAddressBookEntry({ name, address })))
}
@ -94,5 +64,3 @@ const safeStorageMware = (store) => (next) => async (action) => {
return handledAction
}
export default safeStorageMware

View File

@ -1,5 +1,6 @@
import { List, Map, Record, RecordOf, Set } from 'immutable'
import { FEATURES } from 'src/config/networks/network.d'
import { BalanceRecord } from 'src/logic/tokens/store/actions/fetchSafeTokens'
export type SafeOwner = {
name: string
@ -28,14 +29,13 @@ export type SafeRecordProps = {
address: string
threshold: number
ethBalance: string
totalFiatBalance: number
owners: List<SafeOwner>
modules?: ModulePair[] | null
spendingLimits?: SpendingLimit[] | null
activeTokens: Set<string>
activeAssets: Set<string>
blacklistedTokens: Set<string>
blacklistedAssets: Set<string>
balances: Map<string, string>
balances: Map<string, BalanceRecord>
nonce: number
latestIncomingTxBlock: number
recurringUser?: boolean
@ -49,13 +49,12 @@ const makeSafe = Record<SafeRecordProps>({
address: '',
threshold: 0,
ethBalance: '0',
totalFiatBalance: 0,
owners: List([]),
modules: [],
spendingLimits: [],
activeTokens: Set(),
activeAssets: Set(),
blacklistedTokens: Set(),
blacklistedAssets: Set(),
balances: Map(),
nonce: 0,
latestIncomingTxBlock: 0,

View File

@ -1,7 +1,6 @@
import { Map, Set, List } from 'immutable'
import { Action, handleActions } from 'redux-actions'
import { ACTIVATE_TOKEN_FOR_ALL_SAFES } from 'src/logic/safe/store/actions/activateTokenForAllSafes'
import { ADD_SAFE_OWNER } from 'src/logic/safe/store/actions/addSafeOwner'
import { EDIT_SAFE_OWNER } from 'src/logic/safe/store/actions/editSafeOwner'
import { REMOVE_SAFE } from 'src/logic/safe/store/actions/removeSafe'
@ -10,8 +9,6 @@ import { REPLACE_SAFE_OWNER } from 'src/logic/safe/store/actions/replaceSafeOwne
import { SET_DEFAULT_SAFE } from 'src/logic/safe/store/actions/setDefaultSafe'
import { SET_LATEST_MASTER_CONTRACT_VERSION } from 'src/logic/safe/store/actions/setLatestMasterContractVersion'
import { UPDATE_SAFE } from 'src/logic/safe/store/actions/updateSafe'
import { UPDATE_TOKENS_LIST } from 'src/logic/safe/store/actions/updateTokensList'
import { UPDATE_ASSETS_LIST } from 'src/logic/safe/store/actions/updateAssetsList'
import { makeOwner } from 'src/logic/safe/store/models/owner'
import makeSafe, { SafeRecord, SafeRecordProps } from 'src/logic/safe/store/models/safe'
import { AppReduxState } from 'src/store'
@ -29,8 +26,6 @@ export const buildSafe = (storedSafe: SafeRecordProps): SafeRecordProps => {
const owners = buildOwnersFrom(Array.from(names), Array.from(addresses))
const activeTokens = Set(storedSafe.activeTokens)
const activeAssets = Set(storedSafe.activeAssets)
const blacklistedTokens = Set(storedSafe.blacklistedTokens)
const blacklistedAssets = Set(storedSafe.blacklistedAssets)
const balances = Map(storedSafe.balances)
return {
@ -38,9 +33,7 @@ export const buildSafe = (storedSafe: SafeRecordProps): SafeRecordProps => {
owners,
balances,
activeTokens,
blacklistedTokens,
activeAssets,
blacklistedAssets,
latestIncomingTxBlock: 0,
modules: null,
}
@ -102,21 +95,6 @@ export default handleActions<AppReduxState['safes'], Payloads>(
)
: state
},
[ACTIVATE_TOKEN_FOR_ALL_SAFES]: (state, action: Action<SafeRecord>) => {
const tokenAddress = action.payload
return state.withMutations((map) => {
map
.get('safes')
.keySeq()
.forEach((safeAddress) => {
const safeActiveTokens = map.getIn(['safes', safeAddress, 'activeTokens'])
const activeTokens = safeActiveTokens.add(tokenAddress)
map.updateIn(['safes', safeAddress], (prevSafe) => prevSafe.mergeDeep({ activeTokens }))
})
})
},
[ADD_OR_UPDATE_SAFE]: (state, action: Action<SafePayload>) => {
const { safe } = action.payload
const safeAddress = safe.address
@ -195,24 +173,6 @@ export default handleActions<AppReduxState['safes'], Payloads>(
return prevSafe.merge({ owners: updatedOwners })
})
},
[UPDATE_TOKENS_LIST]: (state, action: Action<SafeWithAddressPayload>) => {
// Only activeTokens or blackListedTokens is required
const { safeAddress, activeTokens, blacklistedTokens } = action.payload
const key = activeTokens ? 'activeTokens' : 'blacklistedTokens'
const list = activeTokens ?? blacklistedTokens
return state.updateIn(['safes', safeAddress], (prevSafe) => prevSafe.set(key, list))
},
[UPDATE_ASSETS_LIST]: (state, action: Action<SafeWithAddressPayload>) => {
// Only activeAssets or blackListedAssets is required
const { safeAddress, activeAssets, blacklistedAssets } = action.payload
const key = activeAssets ? 'activeAssets' : 'blacklistedAssets'
const list = activeAssets ?? blacklistedAssets
return state.updateIn(['safes', safeAddress], (prevSafe) => prevSafe.set(key, list))
},
[SET_DEFAULT_SAFE]: (state, action: Action<SafeRecord>) => state.set('defaultSafe', action.payload),
[SET_LATEST_MASTER_CONTRACT_VERSION]: (state, action: Action<SafeRecord>) =>
state.set('latestMasterContractVersion', action.payload),

View File

@ -76,51 +76,6 @@ export const safeActiveTokensSelector = createSelector(
},
)
export const safeActiveAssetsSelector = createSelector(
safeSelector,
(safe): Set<string> => {
if (!safe) {
return Set()
}
return safe.activeAssets
},
)
export const safeActiveAssetsListSelector = createSelector(safeActiveAssetsSelector, (safeList) => {
if (!safeList) {
return Set([])
}
return Set(safeList)
})
export const safeBlacklistedTokensSelector = createSelector(
safeSelector,
(safe): Set<string> => {
if (!safe) {
return Set()
}
return safe.blacklistedTokens
},
)
export const safeBlacklistedAssetsSelector = createSelector(
safeSelector,
(safe): Set<string> => {
if (!safe) {
return Set()
}
return safe.blacklistedAssets
},
)
export const safeActiveAssetsSelectorBySafe = (safeAddress: string, safes: SafesMap): Set<string> =>
safes.get(safeAddress)?.get('activeAssets') || Set()
export const safeBlacklistedAssetsSelectorBySafe = (safeAddress: string, safes: SafesMap): Set<string> =>
safes.get(safeAddress)?.get('blacklistedAssets') || Set()
const baseSafe = makeSafe()
export const safeFieldSelector = <K extends keyof SafeRecordProps>(field: K) => (
@ -172,14 +127,6 @@ export const getActiveTokensAddressesForAllSafes = createSelector(safesListSelec
return addresses
})
export const getBlacklistedTokensAddressesForAllSafes = createSelector(safesListSelector, (safes) => {
const addresses = Set().withMutations((set) => {
safes.forEach((safe) => {
safe.blacklistedTokens.forEach((tokenAddress) => {
set.add(tokenAddress)
})
})
})
return addresses
export const safeFiatBalancesTotalSelector = createSelector(safeSelector, (currentSafe) => {
return currentSafe?.totalFiatBalance.toString()
})

View File

@ -1,59 +0,0 @@
import { Set, Map } from 'immutable'
import { aNewStore } from 'src/store'
import updateActiveTokens from 'src/logic/safe/store/actions/updateActiveTokens'
import '@testing-library/jest-dom/extend-expect'
import updateSafe from 'src/logic/safe/store/actions/updateSafe'
import { makeToken } from 'src/logic/tokens/store/model/token'
import { safesMapSelector } from 'src/logic/safe/store/selectors'
describe('Feature > Balances', () => {
let store
const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf'
beforeEach(async () => {
store = aNewStore()
})
it('It should return an updated balance when updates active tokens', async () => {
// given
const tokensAmount = '100'
const token = makeToken({
address: '0x00Df91984582e6e96288307E9c2f20b38C8FeCE9',
name: 'OmiseGo',
symbol: 'OMG',
decimals: 18,
logoUri:
'https://github.com/TrustWallet/tokens/blob/master/images/0x6810e776880c02933d47db1b9fc05908e5386b96.png?raw=true',
})
const balances = Map({
[token.address]: tokensAmount,
})
const expectedResult = '100'
// when
store.dispatch(updateSafe({ address: safeAddress, balances }))
store.dispatch(updateActiveTokens(safeAddress, Set([token.address])))
const safe = safesMapSelector(store.getState()).get(safeAddress)
const balanceResult = safe?.get('balances').get(token.address)
const activeTokens = safe?.get('activeTokens')
const tokenIsActive = activeTokens?.has(token.address)
// then
expect(balanceResult).toBe(expectedResult)
expect(tokenIsActive).toBe(true)
})
it('The store should have an updated ether balance after updating the value', async () => {
// given
const etherAmount = '1'
const expectedResult = '1'
// when
store.dispatch(updateSafe({ address: safeAddress, ethBalance: etherAmount }))
const safe = safesMapSelector(store.getState()).get(safeAddress)
const balanceResult = safe?.get('ethBalance')
// then
expect(balanceResult).toBe(expectedResult)
})
})

View File

@ -7,9 +7,7 @@ const getMockedOldSafe = ({
needsUpdate,
balances,
recurringUser,
blacklistedAssets,
blacklistedTokens,
activeAssets,
assets,
activeTokens,
owners,
featuresEnabled,
@ -34,8 +32,6 @@ const getMockedOldSafe = ({
const mockedActiveTokenAddress2 = '0x92aF97cbF10742dD2527ffaBA70e34C03CFFC2c1'
const mockedActiveAssetsAddress1 = '0x503ab2a6A70c6C6ec8b25a4C87C784e1c8f8e8CD'
const mockedActiveAssetsAddress2 = '0xfdd4E685361CB7E89a4D27e03DCd0001448d731F'
const mockedBlacklistedTokenAddress1 = '0xc7d892dca37a244Fb1A7461e6141e58Ead460282'
const mockedBlacklistedAssetAddress1 = '0x0ac539137c4c99001f16Dd132E282F99A02Ddc3F'
return {
name: name || 'MockedSafe',
@ -46,14 +42,12 @@ const getMockedOldSafe = ({
modules: modules || [],
spendingLimits: spendingLimits || [],
activeTokens: activeTokens || Set([mockedActiveTokenAddress1, mockedActiveTokenAddress2]),
activeAssets: activeAssets || Set([mockedActiveAssetsAddress1, mockedActiveAssetsAddress2]),
blacklistedTokens: blacklistedTokens || Set([mockedBlacklistedTokenAddress1]),
blacklistedAssets: blacklistedAssets || Set([mockedBlacklistedAssetAddress1]),
assets: assets || Set([mockedActiveAssetsAddress1, mockedActiveAssetsAddress2]),
balances:
balances ||
Map({
[mockedActiveTokenAddress1]: '100',
[mockedActiveTokenAddress2]: '10',
[mockedActiveTokenAddress1]: { tokenBalance: '100' },
[mockedActiveTokenAddress2]: { tokenBalance: '10' },
}),
nonce: nonce || 2,
latestIncomingTxBlock: latestIncomingTxBlock || 1,
@ -61,6 +55,7 @@ const getMockedOldSafe = ({
currentVersion: currentVersion || 'v1.1.1',
needsUpdate: needsUpdate || false,
featuresEnabled: featuresEnabled || [],
totalFiatBalance: 110,
}
}
@ -209,43 +204,9 @@ describe('shouldSafeStoreBeUpdated', () => {
const mockedActiveTokenAddress2 = '0x92aF97cbF10742dD2527ffaBA70e34C03CFFC2c1'
const oldActiveAssets = Set([mockedActiveTokenAddress1, mockedActiveTokenAddress2])
const newActiveAssets = Set([mockedActiveTokenAddress1])
const oldSafe = getMockedOldSafe({ activeAssets: oldActiveAssets })
const oldSafe = getMockedOldSafe({ assets: oldActiveAssets })
const newSafeProps: Partial<SafeRecordProps> = {
activeAssets: newActiveAssets,
}
// When
const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe)
// Then
expect(expectedResult).toEqual(true)
})
it(`Given an old blacklistedTokens list and a new blacklistedTokens list for the safe, should return true`, () => {
// given
const mockedActiveTokenAddress1 = '0x36591cd3DA96b21Ac9ca54cFaf80fe45107294F1'
const mockedActiveTokenAddress2 = '0x92aF97cbF10742dD2527ffaBA70e34C03CFFC2c1'
const oldBlacklistedTokens = Set([mockedActiveTokenAddress1, mockedActiveTokenAddress2])
const newBlacklistedTokens = Set([mockedActiveTokenAddress1])
const oldSafe = getMockedOldSafe({ blacklistedTokens: oldBlacklistedTokens })
const newSafeProps: Partial<SafeRecordProps> = {
blacklistedTokens: newBlacklistedTokens,
}
// When
const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe)
// Then
expect(expectedResult).toEqual(true)
})
it(`Given an old blacklistedAssets list and a new blacklistedAssets list for the safe, should return true`, () => {
// given
const mockedActiveTokenAddress1 = '0x36591cd3DA96b21Ac9ca54cFaf80fe45107294F1'
const mockedActiveTokenAddress2 = '0x92aF97cbF10742dD2527ffaBA70e34C03CFFC2c1'
const oldBlacklistedAssets = Set([mockedActiveTokenAddress1, mockedActiveTokenAddress2])
const newBlacklistedAssets = Set([mockedActiveTokenAddress1])
const oldSafe = getMockedOldSafe({ blacklistedAssets: oldBlacklistedAssets })
const newSafeProps: Partial<SafeRecordProps> = {
blacklistedAssets: newBlacklistedAssets,
assets: newActiveAssets,
}
// When
@ -259,11 +220,11 @@ describe('shouldSafeStoreBeUpdated', () => {
const mockedActiveTokenAddress1 = '0x36591cd3DA96b21Ac9ca54cFaf80fe45107294F1'
const mockedActiveTokenAddress2 = '0x92aF97cbF10742dD2527ffaBA70e34C03CFFC2c1'
const oldBalances = Map({
[mockedActiveTokenAddress1]: '100',
[mockedActiveTokenAddress2]: '10',
[mockedActiveTokenAddress1]: { tokenBalance: '100' },
[mockedActiveTokenAddress2]: { tokenBalance: '100' },
})
const newBalances = Map({
[mockedActiveTokenAddress1]: '100',
[mockedActiveTokenAddress1]: { tokenBalance: '100' },
})
const oldSafe = getMockedOldSafe({ balances: oldBalances })
const newSafeProps: Partial<SafeRecordProps> = {

View File

@ -1,44 +0,0 @@
import { nftAssetsSelector } from 'src/logic/collectibles/store/selectors'
import updateActiveAssets from 'src/logic/safe/store/actions/updateActiveAssets'
import {
safeActiveAssetsSelectorBySafe,
safeBlacklistedAssetsSelectorBySafe,
safesMapSelector,
} from 'src/logic/safe/store/selectors'
const activateAssetsByBalance = (safeAddress) => async (dispatch, getState) => {
try {
const state = getState()
const safes = safesMapSelector(state)
if (safes.size === 0) {
return
}
const availableAssets = nftAssetsSelector(state)
const alreadyActiveAssets = safeActiveAssetsSelectorBySafe(safeAddress, safes)
const blacklistedAssets = safeBlacklistedAssetsSelectorBySafe(safeAddress, safes)
// active tokens by balance, excluding those already blacklisted and the `null` address
const activeByBalance = Object.entries(availableAssets)
.filter((asset) => {
const { address, numberOfTokens }: any = asset[1]
return address !== null && !blacklistedAssets.has(address) && numberOfTokens > 0
})
.map((asset) => {
return asset[0]
})
// need to persist those already active assets, despite its balances
const activeAssets = alreadyActiveAssets.union(activeByBalance)
// update list of active tokens
dispatch(updateActiveAssets(safeAddress, activeAssets))
} catch (err) {
console.error('Error fetching active assets list', err)
}
return null
}
export default activateAssetsByBalance

View File

@ -2,8 +2,6 @@ import { createAction } from 'redux-actions'
export const ADD_TOKENS = 'ADD_TOKENS'
const addTokens = createAction(ADD_TOKENS, (tokens) => ({
export const addTokens = createAction(ADD_TOKENS, (tokens) => ({
tokens,
}))
export default addTokens

View File

@ -2,63 +2,56 @@ import { backOff } from 'exponential-backoff'
import { List, Map } from 'immutable'
import { Dispatch } from 'redux'
import { fetchTokenCurrenciesBalances, TokenBalance } from 'src/logic/currencyValues/api/fetchTokenCurrenciesBalances'
import {
AVAILABLE_CURRENCIES,
CurrencyRateValueRecord,
makeBalanceCurrency,
} from 'src/logic/currencyValues/store/model/currencyValues'
import addTokens from 'src/logic/tokens/store/actions/saveTokens'
import { fetchTokenCurrenciesBalances, TokenBalance } from 'src/logic/safe/api/fetchTokenCurrenciesBalances'
import { addTokens } from 'src/logic/tokens/store/actions/addTokens'
import { makeToken, Token } from 'src/logic/tokens/store/model/token'
import { TokenState } from 'src/logic/tokens/store/reducer/tokens'
import updateSafe from 'src/logic/safe/store/actions/updateSafe'
import { AppReduxState } from 'src/store'
import { humanReadableValue } from 'src/logic/tokens/utils/humanReadableValue'
import { safeActiveTokensSelector, safeBlacklistedTokensSelector, safeSelector } from 'src/logic/safe/store/selectors'
import { safeActiveTokensSelector, safeSelector } from 'src/logic/safe/store/selectors'
import { tokensSelector } from 'src/logic/tokens/store/selectors'
import { currentCurrencySelector } from 'src/logic/currencyValues/store/selectors'
import { sameAddress, ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
import { setCurrencyBalances } from 'src/logic/currencyValues/store/actions/setCurrencyBalances'
import { getNetworkInfo } from 'src/config'
import BigNumber from 'bignumber.js'
import { currentCurrencySelector } from 'src/logic/currencyValues/store/selectors'
export type BalanceRecord = {
tokenBalance: string
fiatBalance?: string
}
interface ExtractedData {
balances: Map<string, string>
currencyList: List<CurrencyRateValueRecord>
balances: Map<string, BalanceRecord>
ethBalance: string
tokens: List<Token>
}
const { nativeCoin } = getNetworkInfo()
const extractDataFromResult = (currentTokens: TokenState, fiatCode: string) => (
const extractDataFromResult = (currentTokens: TokenState) => (
acc: ExtractedData,
{ balance, fiatBalance, tokenInfo }: TokenBalance,
): ExtractedData => {
const { address: tokenAddress, decimals } = tokenInfo
if (sameAddress(tokenAddress, ZERO_ADDRESS) || sameAddress(tokenAddress, nativeCoin.address)) {
acc.ethBalance = humanReadableValue(balance, 18)
} else {
acc.balances = acc.balances.merge({ [tokenAddress]: humanReadableValue(balance, Number(decimals)) })
if (currentTokens && !currentTokens.get(tokenAddress)) {
acc.tokens = acc.tokens.push(makeToken({ ...tokenInfo }))
}
}
acc.balances = acc.balances.merge({
[tokenAddress]: {
fiatBalance,
tokenBalance: humanReadableValue(balance, Number(decimals)),
},
})
acc.currencyList = acc.currencyList.push(
makeBalanceCurrency({
currencyName: fiatCode,
tokenAddress,
balanceInBaseCurrency: fiatBalance,
balanceInSelectedCurrency: fiatBalance,
}),
)
if (currentTokens && !currentTokens.get(tokenAddress)) {
acc.tokens = acc.tokens.push(makeToken({ ...tokenInfo }))
}
return acc
}
const fetchSafeTokens = (safeAddress: string) => async (
export const fetchSafeTokens = (safeAddress: string, currencySelected?: string) => async (
dispatch: Dispatch,
getState: () => AppReduxState,
): Promise<void> => {
@ -66,38 +59,40 @@ const fetchSafeTokens = (safeAddress: string) => async (
const state = getState()
const safe = safeSelector(state)
const currentTokens = tokensSelector(state)
const currencySelected = currentCurrencySelector(state)
if (!safe) {
return
}
const selectedCurrency = currentCurrencySelector(state)
const tokenCurrenciesBalances = await backOff(() => fetchTokenCurrenciesBalances(safeAddress))
const tokenCurrenciesBalances = await backOff(() =>
fetchTokenCurrenciesBalances({ safeAddress, selectedCurrency: currencySelected ?? selectedCurrency }),
)
const alreadyActiveTokens = safeActiveTokensSelector(state)
const blacklistedTokens = safeBlacklistedTokensSelector(state)
const { balances, currencyList, ethBalance, tokens } = tokenCurrenciesBalances.items.reduce<ExtractedData>(
extractDataFromResult(currentTokens, currencySelected || AVAILABLE_CURRENCIES.USD),
const { balances, ethBalance, tokens } = tokenCurrenciesBalances.items.reduce<ExtractedData>(
extractDataFromResult(currentTokens),
{
balances: Map(),
currencyList: List(),
ethBalance: '0',
tokens: List(),
},
)
// need to persist those already active tokens, despite its balances
const activeTokens = alreadyActiveTokens.union(
// active tokens by balance, excluding those already blacklisted and the `null` address
balances.keySeq().toSet().subtract(blacklistedTokens),
)
const activeTokens = alreadyActiveTokens.union(balances.keySeq().toSet())
dispatch(updateSafe({ address: safeAddress, activeTokens, balances, ethBalance }))
dispatch(setCurrencyBalances(safeAddress, currencyList))
dispatch(
updateSafe({
address: safeAddress,
activeTokens,
balances,
ethBalance,
totalFiatBalance: new BigNumber(tokenCurrenciesBalances.fiatTotal).toFixed(2),
}),
)
dispatch(addTokens(tokens))
} catch (err) {
console.error('Error fetching active token list', err)
}
}
export default fetchSafeTokens

View File

@ -5,9 +5,7 @@ import ERC721 from '@openzeppelin/contracts/build/contracts/ERC721.json'
import { List } from 'immutable'
import contract from '@truffle/contract/index.js'
import { AbiItem } from 'web3-utils'
import saveTokens from './saveTokens'
import { addTokens } from 'src/logic/tokens/store/actions/addTokens'
import generateBatchRequests from 'src/logic/contracts/generateBatchRequests'
import { fetchErc20AndErc721AssetsList } from 'src/logic/tokens/api'
import { makeToken, Token } from 'src/logic/tokens/store/model/token'
@ -85,7 +83,7 @@ export const getTokenInfos = async (tokenAddress: string): Promise<Token | undef
})
const newTokens = tokens.set(tokenAddress, token)
store.dispatch(saveTokens(newTokens))
store.dispatch(addTokens(newTokens))
return token
}
@ -109,10 +107,8 @@ export const fetchTokens = () => async (
const tokens = List(erc20Tokens.map((token) => makeToken(token)))
dispatch(saveTokens(tokens))
dispatch(addTokens(tokens))
} catch (err) {
console.error('Error fetching token list', err)
}
}
export default fetchTokens

View File

@ -1,25 +0,0 @@
import { List } from 'immutable'
import saveTokens from './saveTokens'
import { makeToken } from 'src/logic/tokens/store/model/token'
import { getActiveTokens } from 'src/logic/tokens/utils/tokensStorage'
const loadActiveTokens = () => async (dispatch) => {
try {
const tokens = (await getActiveTokens()) || {}
// The filter of strings was made because of the issue #751. Please see: https://github.com/gnosis/safe-react/pull/755#issuecomment-612969340
const tokenRecordsList = List(
Object.values(tokens)
.filter((t: any) => typeof t.decimals !== 'string')
.map((token) => makeToken(token)),
)
dispatch(saveTokens(tokenRecordsList))
} catch (err) {
// eslint-disable-next-line
console.error('Error while loading active tokens from storage:', err)
}
}
export default loadActiveTokens

View File

@ -1,5 +1,6 @@
import { Record, RecordOf } from 'immutable'
import { TokenType } from 'src/logic/safe/store/models/types/gateway'
import { BalanceRecord } from 'src/logic/tokens/store/actions/fetchSafeTokens'
export type TokenProps = {
address: string
@ -7,7 +8,7 @@ export type TokenProps = {
symbol: string
decimals: number | string
logoUri: string
balance: number | string
balance: BalanceRecord
type?: TokenType
}
@ -17,7 +18,10 @@ export const makeToken = Record<TokenProps>({
symbol: '',
decimals: 0,
logoUri: '',
balance: 0,
balance: {
fiatBalance: '0',
tokenBalance: '0',
},
})
// balance is only set in extendedSafeTokensSelector when we display user's token balances

View File

@ -2,7 +2,7 @@ import { List, Map } from 'immutable'
import { Action, handleActions } from 'redux-actions'
import { ADD_TOKEN } from 'src/logic/tokens/store/actions/addToken'
import { ADD_TOKENS } from 'src/logic/tokens/store/actions/saveTokens'
import { ADD_TOKENS } from 'src/logic/tokens/store/actions/addTokens'
import { makeToken, Token } from 'src/logic/tokens/store/model/token'
import { AppReduxState } from 'src/store'

View File

@ -15,7 +15,9 @@ export const getEthAsToken = (balance: string | number): Token => {
const { nativeCoin } = getNetworkInfo()
return makeToken({
...nativeCoin,
balance,
balance: {
tokenBalance: balance.toString(),
},
})
}
@ -73,7 +75,7 @@ export type GetTokenByAddress = {
tokens: List<Token>
}
export type TokenFound = {
type TokenFound = {
balance: string | number
decimals: string | number
}
@ -92,7 +94,7 @@ export const getBalanceAndDecimalsFromToken = ({ tokenAddress, tokens }: GetToke
}
return {
balance: token.balance ?? 0,
balance: token.balance.tokenBalance ?? 0,
decimals: token.decimals ?? 0,
}
}

View File

@ -1,25 +0,0 @@
import { Map } from 'immutable'
import { loadFromStorage, saveToStorage } from 'src/utils/storage'
import { TokenProps, Token } from './../store/model/token'
export const ACTIVE_TOKENS_KEY = 'ACTIVE_TOKENS'
export const CUSTOM_TOKENS_KEY = 'CUSTOM_TOKENS'
// Tokens which are active at least in one of used Safes in the app should be saved to localstorage
// to avoid iterating a large amount of data of tokens from the backend
// Custom tokens should be saved too unless they're deleted (marking them as inactive doesn't count)
export const saveActiveTokens = async (tokens: Map<string, Token>): Promise<void> => {
try {
await saveToStorage(ACTIVE_TOKENS_KEY, tokens.toJS() as Record<string, TokenProps>)
} catch (err) {
console.error('Error storing tokens in localstorage', err)
}
}
export const getActiveTokens = async (): Promise<Record<string, TokenProps> | undefined> => {
const data = await loadFromStorage<Record<string, TokenProps>>(ACTIVE_TOKENS_KEY)
return data
}

View File

@ -14,11 +14,6 @@ import Table from 'src/components/Table'
import { cellWidth } from 'src/components/Table/TableHead'
import Button from 'src/components/layout/Button'
import Row from 'src/components/layout/Row'
import {
currencyRateSelector,
currentCurrencySelector,
safeFiatBalancesListSelector,
} from 'src/logic/currencyValues/store/selectors'
import { BALANCE_ROW_TEST_ID } from 'src/routes/safe/components/Balances'
import AssetTableCell from 'src/routes/safe/components/Balances/AssetTableCell'
import {
@ -33,6 +28,7 @@ import { extendedSafeTokensSelector, grantedSelector } from 'src/routes/safe/con
import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics'
import { makeStyles } from '@material-ui/core/styles'
import { styles } from './styles'
import { currentCurrencySelector } from 'src/logic/currencyValues/store/selectors'
const useStyles = makeStyles(styles)
@ -69,9 +65,7 @@ const Coins = (props: Props): React.ReactElement => {
const columns = generateColumns()
const autoColumns = columns.filter((c) => !c.custom)
const selectedCurrency = useSelector(currentCurrencySelector)
const currencyRate = useSelector(currencyRateSelector)
const activeTokens = useSelector(extendedSafeTokensSelector)
const currencyValues = useSelector(safeFiatBalancesListSelector)
const granted = useSelector(grantedSelector)
const { trackEvent } = useAnalytics()
@ -79,10 +73,10 @@ const Coins = (props: Props): React.ReactElement => {
trackEvent({ category: SAFE_NAVIGATION_EVENT, action: 'Coins' })
}, [trackEvent])
const filteredData: List<BalanceData> = useMemo(
() => getBalanceData(activeTokens, selectedCurrency, currencyValues, currencyRate),
[activeTokens, selectedCurrency, currencyValues, currencyRate],
)
const filteredData: List<BalanceData> = useMemo(() => getBalanceData(activeTokens, selectedCurrency), [
activeTokens,
selectedCurrency,
])
return (
<TableContainer>

View File

@ -18,7 +18,7 @@ import { ScanQRWrapper } from 'src/components/ScanQRModal/ScanQRWrapper'
import WhenFieldChanges from 'src/components/WhenFieldChanges'
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
import { getNameFromAddressBook } from 'src/logic/addressBook/utils'
import { nftTokensSelector, safeActiveSelectorMap } from 'src/logic/collectibles/store/selectors'
import { nftAssetsSelector, nftTokensSelector } from 'src/logic/collectibles/store/selectors'
import { Erc721Transfer } from 'src/logic/safe/store/models/types/gateway'
import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo'
import { AddressBookInput } from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput'
@ -71,7 +71,7 @@ const SendCollectible = ({
selectedToken,
}: SendCollectibleProps): React.ReactElement => {
const classes = useStyles()
const nftAssets = useSelector(safeActiveSelectorMap)
const nftAssets = useSelector(nftAssetsSelector)
const nftTokens = useSelector(nftTokensSelector)
const addressBook = useSelector(addressBookSelector)
const [selectedEntry, setSelectedEntry] = useState<{ address: string; name: string } | null>(() => {

View File

@ -208,7 +208,7 @@ const SendFunds = ({
const setMaxAllowedAmount = () => {
const isSpendingLimit = tokenSpendingLimit && txType === 'spendingLimit'
let maxAmount = selectedToken?.balance ?? 0
let maxAmount = selectedToken?.balance.tokenBalance ?? 0
if (isSpendingLimit) {
const spendingLimitBalance = fromTokenUnit(

View File

@ -1,60 +0,0 @@
import IconButton from '@material-ui/core/IconButton'
import { makeStyles } from '@material-ui/core/styles'
import Close from '@material-ui/icons/Close'
import React from 'react'
import { useSelector } from 'react-redux'
import { styles } from './style'
import Hairline from 'src/components/layout/Hairline'
import Paragraph from 'src/components/layout/Paragraph'
import Row from 'src/components/layout/Row'
import { orderedTokenListSelector } from 'src/logic/tokens/store/selectors'
import { AssetsList } from 'src/routes/safe/components/Balances/Tokens/screens/AssetsList'
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 useStyles = makeStyles(styles)
type Props = {
safeAddress: string
modalScreen: string
onClose: () => void
}
export 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()
return (
<>
<Row align="center" className={classes.heading} grow>
<Paragraph noMargin size="xl" weight="bolder">
Manage List
</Paragraph>
<IconButton data-testid={MANAGE_TOKENS_MODAL_CLOSE_BUTTON_TEST_ID} disableRipple onClick={onClose}>
<Close className={classes.close} />
</IconButton>
</Row>
<Hairline />
{modalScreen === 'tokenList' && (
<TokenList
activeTokens={activeTokens}
blacklistedTokens={blacklistedTokens}
safeAddress={safeAddress}
tokens={tokens}
/>
)}
{modalScreen === 'assetsList' && <AssetsList />}
</>
)
}

View File

@ -1,47 +0,0 @@
import ListItem from '@material-ui/core/ListItem'
import ListItemIcon from '@material-ui/core/ListItemIcon'
import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'
import ListItemText from '@material-ui/core/ListItemText'
import Switch from '@material-ui/core/Switch'
import React, { memo } from 'react'
import { useStyles } from 'src/routes/safe/components/Balances/Tokens/screens/TokenList/style'
import Img from 'src/components/layout/Img'
import { getNetworkInfo } from 'src/config'
import { setCollectibleImageToPlaceholder } from 'src/routes/safe/components/Balances/utils'
export const TOGGLE_ASSET_TEST_ID = 'toggle-asset-btn'
const { nativeCoin } = getNetworkInfo()
const AssetRow = memo(({ data, index, style }: any) => {
const classes = useStyles()
const { activeAssetsAddresses, assets, onSwitch } = data
const asset = assets[index]
const { address, image, name, symbol } = asset
const isActive = activeAssetsAddresses.includes(asset.address)
return (
<div style={style}>
<ListItem classes={{ root: classes.tokenRoot }} className={classes.token}>
<ListItemIcon className={classes.tokenIcon}>
<Img alt={name} height={28} onError={setCollectibleImageToPlaceholder} src={image} />
</ListItemIcon>
<ListItemText primary={symbol} secondary={name} />
{address !== nativeCoin.address && (
<ListItemSecondaryAction>
<Switch
checked={isActive}
inputProps={{ 'data-testid': `${symbol}_${TOGGLE_ASSET_TEST_ID}` } as any}
onChange={onSwitch(asset)}
/>
</ListItemSecondaryAction>
)}
</ListItem>
</div>
)
})
AssetRow.displayName = 'AssetRow'
export default AssetRow

View File

@ -1,132 +0,0 @@
import MuiList from '@material-ui/core/List'
import CircularProgress from '@material-ui/core/CircularProgress'
import Search from '@material-ui/icons/Search'
import cn from 'classnames'
import SearchBar from 'material-ui-search-bar'
import React, { useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { FixedSizeList } from 'react-window'
import Paragraph from 'src/components/layout/Paragraph'
import { useStyles } from './style'
import Block from 'src/components/layout/Block'
import Hairline from 'src/components/layout/Hairline'
import Row from 'src/components/layout/Row'
import { nftAssetsListSelector } from 'src/logic/collectibles/store/selectors'
import AssetRow from 'src/routes/safe/components/Balances/Tokens/screens/AssetsList/AssetRow'
import updateActiveAssets from 'src/logic/safe/store/actions/updateActiveAssets'
import updateBlacklistedAssets from 'src/logic/safe/store/actions/updateBlacklistedAssets'
import {
safeActiveAssetsListSelector,
safeBlacklistedAssetsSelector,
safeParamAddressFromStateSelector,
} from 'src/logic/safe/store/selectors'
const filterBy = (filter, nfts) =>
nfts.filter(
(asset) =>
!filter ||
asset.description.toLowerCase().includes(filter.toLowerCase()) ||
asset.name.toLowerCase().includes(filter.toLowerCase()) ||
asset.symbol.toLowerCase().includes(filter.toLowerCase()),
)
export const AssetsList = (): React.ReactElement => {
const classes = useStyles()
const searchClasses = {
input: classes.searchInput,
root: classes.searchRoot,
iconButton: classes.searchIcon,
searchContainer: classes.searchContainer,
}
const dispatch = useDispatch()
const activeAssetsList = useSelector(safeActiveAssetsListSelector)
const blacklistedAssets = useSelector(safeBlacklistedAssetsSelector)
const safeAddress = useSelector(safeParamAddressFromStateSelector)
const [filterValue, setFilterValue] = useState('')
const [activeAssetsAddresses, setActiveAssetsAddresses] = useState(activeAssetsList)
const [blacklistedAssetsAddresses, setBlacklistedAssetsAddresses] = useState(blacklistedAssets)
const nftAssetsList = useSelector(nftAssetsListSelector)
const onCancelSearch = () => {
setFilterValue('')
}
const onChangeSearchBar = (value) => {
setFilterValue(value)
}
const getItemKey = (index) => {
return index
}
const onSwitch = (asset) => () => {
let newActiveAssetsAddresses
let newBlacklistedAssetsAddresses
if (activeAssetsAddresses.has(asset.address)) {
newActiveAssetsAddresses = activeAssetsAddresses.delete(asset.address)
newBlacklistedAssetsAddresses = blacklistedAssetsAddresses.add(asset.address)
} else {
newActiveAssetsAddresses = activeAssetsAddresses.add(asset.address)
newBlacklistedAssetsAddresses = blacklistedAssetsAddresses.delete(asset.address)
}
// Set local state
setActiveAssetsAddresses(newActiveAssetsAddresses)
setBlacklistedAssetsAddresses(newBlacklistedAssetsAddresses)
// Dispatch to global state
dispatch(updateActiveAssets(safeAddress, newActiveAssetsAddresses))
dispatch(updateBlacklistedAssets(safeAddress, newBlacklistedAssetsAddresses))
}
const createItemData = (assetsList) => {
return {
assets: assetsList,
activeAssetsAddresses,
onSwitch,
}
}
const nftAssetsFilteredList = filterBy(filterValue, nftAssetsList)
const itemData = createItemData(nftAssetsFilteredList)
return (
<>
<Block className={classes.root}>
<Row align="center" className={cn(classes.padding, classes.actions)}>
<Search className={classes.search} />
<SearchBar
classes={searchClasses}
onCancelSearch={onCancelSearch}
onChange={onChangeSearchBar}
placeholder="Search by name or symbol"
searchIcon={<div />}
value={filterValue}
/>
</Row>
<Hairline />
</Block>
{!nftAssetsList?.length && (
<Block className={classes.progressContainer} justify="center">
{!nftAssetsList ? <CircularProgress /> : <Paragraph>No collectibles available</Paragraph>}
</Block>
)}
{nftAssetsFilteredList.length > 0 && (
<MuiList className={classes.list}>
<FixedSizeList
height={413}
itemCount={nftAssetsFilteredList.length}
itemData={itemData}
itemKey={getItemKey}
itemSize={51}
overscanCount={process.env.NODE_ENV === 'test' ? 100 : 10}
width={500}
>
{AssetRow}
</FixedSizeList>
</MuiList>
)}
</>
)
}

View File

@ -1,79 +0,0 @@
import { createStyles, makeStyles } from '@material-ui/core'
import { md, mediumFontSize, secondaryText, sm, xs } from 'src/theme/variables'
export const useStyles = makeStyles(
createStyles({
root: {
minHeight: '52px',
},
search: {
color: secondaryText,
paddingLeft: sm,
},
padding: {
padding: `0 ${md}`,
},
add: {
fontSize: '11px',
fontWeight: 'normal',
paddingRight: md,
paddingLeft: md,
},
addBtnLabel: {
fontSize: mediumFontSize,
},
actions: {
height: '50px',
},
list: {
overflow: 'hidden',
overflowY: 'scroll',
padding: 0,
height: '100%',
},
tokenIcon: {
marginRight: sm,
height: '28px',
width: '28px',
},
searchInput: {
backgroundColor: 'transparent',
lineHeight: 'initial',
fontSize: '13px',
padding: 0,
'& > input::placeholder': {
letterSpacing: '-0.5px',
fontSize: mediumFontSize,
color: 'black',
},
'& > input': {
letterSpacing: '-0.5px',
},
},
progressContainer: {
width: '100%',
height: '100%',
alignItems: 'center',
},
searchContainer: {
marginLeft: xs,
marginRight: xs,
},
searchRoot: {
letterSpacing: '-0.5px',
fontSize: '13px',
border: 'none',
boxShadow: 'none',
'& > button': {
display: 'none',
},
flex: 1,
},
searchIcon: {
'&:hover': {
backgroundColor: 'transparent !important',
},
},
}),
)

View File

@ -1,54 +0,0 @@
import ListItem from '@material-ui/core/ListItem'
import ListItemIcon from '@material-ui/core/ListItemIcon'
import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'
import ListItemText from '@material-ui/core/ListItemText'
import Switch from '@material-ui/core/Switch'
import React, { CSSProperties, memo, ReactElement } from 'react'
import { useStyles } from './style'
import Img from 'src/components/layout/Img'
import { getNetworkInfo } from 'src/config'
import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils'
import { ItemData } from 'src/routes/safe/components/Balances/Tokens/screens/TokenList/index'
export const TOGGLE_TOKEN_TEST_ID = 'toggle-token-btn'
interface TokenRowProps {
data: ItemData
index: number
style: CSSProperties
}
const { nativeCoin } = getNetworkInfo()
const TokenRow = memo(({ data, index, style }: TokenRowProps): ReactElement | null => {
const classes = useStyles()
const { activeTokensAddresses, onSwitch, tokens } = data
const token = tokens.get(index)
if (!token) {
return null
}
const isActive = activeTokensAddresses.has(token.address)
return (
<div style={style}>
<ListItem classes={{ root: classes.tokenRoot }} className={classes.token}>
<ListItemIcon className={classes.tokenIcon}>
<Img alt={token.name} height={28} onError={setImageToPlaceholder} src={token.logoUri} />
</ListItemIcon>
<ListItemText primary={token.symbol} secondary={token.name} />
{token.address !== nativeCoin.address && (
<ListItemSecondaryAction data-testid={`${token.symbol}_${TOGGLE_TOKEN_TEST_ID}`}>
<Switch checked={isActive} onChange={onSwitch(token)} />
</ListItemSecondaryAction>
)}
</ListItem>
</div>
)
})
TokenRow.displayName = 'TokenRow'
export default TokenRow

View File

@ -1,136 +0,0 @@
import CircularProgress from '@material-ui/core/CircularProgress'
import MuiList from '@material-ui/core/List'
import Search from '@material-ui/icons/Search'
import cn from 'classnames'
import { List, Set } from 'immutable'
import SearchBar from 'material-ui-search-bar'
import React, { useState } from 'react'
import { FixedSizeList } from 'react-window'
import TokenRow from './TokenRow'
import { useStyles } from './style'
import Block from 'src/components/layout/Block'
import Hairline from 'src/components/layout/Hairline'
import Row from 'src/components/layout/Row'
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: string, tokens: List<Token>): List<Token> =>
tokens.filter(
(token) =>
!filter ||
token.symbol.toLowerCase().includes(filter.toLowerCase()) ||
token.name.toLowerCase().includes(filter.toLowerCase()),
)
type Props = {
tokens: List<Token>
activeTokens: List<Token>
blacklistedTokens: Set<string>
safeAddress: string
}
export type ItemData = {
tokens: List<Token>
activeTokensAddresses: Set<string>
onSwitch: (token: Token) => () => void
}
export const TokenList = (props: Props): React.ReactElement => {
const classes = useStyles()
const { 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()
const searchClasses = {
input: classes.searchInput,
root: classes.searchRoot,
iconButton: classes.searchIcon,
searchContainer: classes.searchContainer,
}
const onCancelSearch = () => {
setFilter('')
}
const onChangeSearchBar = (value: string) => {
setFilter(value)
}
const onSwitch = (token: Token) => () => {
let newActiveTokensAddresses
let newBlacklistedTokensAddresses
if (activeTokensAddresses.has(token.address)) {
newActiveTokensAddresses = activeTokensAddresses.delete(token.address)
newBlacklistedTokensAddresses = blacklistedTokensAddresses.add(token.address)
} else {
newActiveTokensAddresses = activeTokensAddresses.add(token.address)
newBlacklistedTokensAddresses = blacklistedTokensAddresses.delete(token.address)
}
// Set local state
setActiveTokensAddresses(newActiveTokensAddresses)
setBlacklistedTokensAddresses(newBlacklistedTokensAddresses)
// Dispatch to global state
dispatch(updateActiveTokens(safeAddress, newActiveTokensAddresses))
dispatch(updateBlacklistedTokens(safeAddress, newBlacklistedTokensAddresses))
}
const createItemData = (tokens: List<Token>, activeTokensAddresses: Set<string>): ItemData => ({
tokens,
activeTokensAddresses,
onSwitch,
})
const getItemKey = (index: number, { tokens }): string => {
return tokens.get(index).address
}
const filteredTokens = filterBy(filter, tokens)
const itemData = createItemData(filteredTokens, activeTokensAddresses)
return (
<>
<Block className={classes.root}>
<Row align="center" className={cn(classes.padding, classes.actions)}>
<Search className={classes.search} />
<SearchBar
classes={searchClasses}
onCancelSearch={onCancelSearch}
onChange={onChangeSearchBar}
placeholder="Search by name or symbol"
searchIcon={<div />}
value={filter}
/>
</Row>
<Hairline />
</Block>
{!tokens.size && (
<Block className={classes.progressContainer} justify="center">
<CircularProgress />
</Block>
)}
{tokens.size > 0 && (
<MuiList className={classes.list}>
<FixedSizeList
height={413}
itemCount={filteredTokens.size}
itemData={itemData}
itemKey={getItemKey}
itemSize={51}
overscanCount={process.env.NODE_ENV === 'test' ? 100 : 10}
width={500}
>
{TokenRow}
</FixedSizeList>
</MuiList>
)}
</>
)
}

View File

@ -1,87 +0,0 @@
import { createStyles, makeStyles } from '@material-ui/core'
import { border, md, mediumFontSize, secondaryText, sm, xs } from 'src/theme/variables'
export const useStyles = makeStyles(
createStyles({
root: {
minHeight: '52px',
},
search: {
color: secondaryText,
paddingLeft: sm,
},
padding: {
padding: `0 ${md}`,
},
add: {
fontSize: '11px',
fontWeight: 'normal',
paddingRight: md,
paddingLeft: md,
},
addBtnLabel: {
fontSize: mediumFontSize,
},
actions: {
height: '50px',
},
list: {
overflow: 'hidden',
overflowY: 'scroll',
padding: 0,
height: '100%',
},
token: {
minHeight: '50px',
borderBottom: `1px solid ${border}`,
},
tokenRoot: {
paddingTop: 0,
paddingBottom: 0,
},
searchInput: {
backgroundColor: 'transparent',
lineHeight: 'initial',
fontSize: '13px',
padding: 0,
'& > input::placeholder': {
letterSpacing: '-0.5px',
fontSize: mediumFontSize,
color: 'black',
},
'& > input': {
letterSpacing: '-0.5px',
},
},
tokenIcon: {
marginRight: md,
height: '28px',
width: '28px',
},
progressContainer: {
width: '100%',
height: '100%',
alignItems: 'center',
},
searchContainer: {
marginLeft: xs,
marginRight: xs,
},
searchRoot: {
letterSpacing: '-0.5px',
fontSize: '13px',
border: 'none',
boxShadow: 'none',
'& > button': {
display: 'none',
},
flex: 1,
},
searchIcon: {
'&:hover': {
backgroundColor: 'transparent !important',
},
},
}),
)

View File

@ -1,15 +0,0 @@
import { lg, md } from 'src/theme/variables'
import { createStyles } from '@material-ui/core'
export const styles = createStyles({
heading: {
padding: `${md} ${lg}`,
justifyContent: 'space-between',
maxHeight: '75px',
boxSizing: 'border-box',
},
close: {
height: '35px',
width: '35px',
},
})

View File

@ -1,32 +1,13 @@
import { BigNumber } from 'bignumber.js'
import { List } from 'immutable'
import { getNetworkInfo } from 'src/config'
import { FIXED } from 'src/components/Table/sorting'
import { formatAmountInUsFormat } from 'src/logic/tokens/utils/formatAmount'
import { TableColumn } from 'src/components/Table/types.d'
import { BalanceCurrencyList } from 'src/logic/currencyValues/store/model/currencyValues'
import { Token } from 'src/logic/tokens/store/model/token'
import { sameAddress } from 'src/logic/wallets/ethAddresses'
export const BALANCE_TABLE_ASSET_ID = 'asset'
export const BALANCE_TABLE_BALANCE_ID = 'balance'
export const BALANCE_TABLE_VALUE_ID = 'value'
const { nativeCoin } = getNetworkInfo()
const getTokenValue = (token: Token, currencyValues: BalanceCurrencyList, currencyRate: number): string => {
const currencyValue = currencyValues.find(
({ tokenAddress }) => sameAddress(token.address, tokenAddress) || sameAddress(token.address, nativeCoin.address),
)
if (!currencyValue) {
return ''
}
const { balanceInBaseCurrency } = currencyValue
return new BigNumber(balanceInBaseCurrency).times(currencyRate).toString()
}
const getTokenPriceInCurrency = (balance: string, currencySelected?: string): string => {
if (!currencySelected) {
return Number('').toFixed(2)
@ -44,15 +25,10 @@ export interface BalanceData {
valueOrder: number
}
export const getBalanceData = (
activeTokens: List<Token>,
currencySelected?: string,
currencyValues?: BalanceCurrencyList,
currencyRate?: number,
): List<BalanceData> => {
export const getBalanceData = (activeTokens: List<Token>, currencySelected?: string): List<BalanceData> => {
const { nativeCoin } = getNetworkInfo()
return activeTokens.map((token) => {
const balance = currencyRate && currencyValues ? getTokenValue(token, currencyValues, currencyRate) : '0'
const { tokenBalance, fiatBalance } = token.balance
return {
[BALANCE_TABLE_ASSET_ID]: {
@ -62,11 +38,11 @@ export const getBalanceData = (
symbol: token.symbol,
},
assetOrder: token.name,
[BALANCE_TABLE_BALANCE_ID]: `${formatAmountInUsFormat(token.balance?.toString() || '0')} ${token.symbol}`,
balanceOrder: Number(token.balance),
[BALANCE_TABLE_BALANCE_ID]: `${formatAmountInUsFormat(tokenBalance?.toString() || '0')} ${token.symbol}`,
balanceOrder: Number(tokenBalance),
[FIXED]: token.symbol === nativeCoin.symbol,
[BALANCE_TABLE_VALUE_ID]: getTokenPriceInCurrency(balance, currencySelected),
valueOrder: Number(balance),
[BALANCE_TABLE_VALUE_ID]: getTokenPriceInCurrency(fiatBalance || '0', currencySelected),
valueOrder: Number(tokenBalance),
}
})
}
@ -78,6 +54,7 @@ export const generateColumns = (): List<TableColumn> => {
disablePadding: false,
label: 'Asset',
custom: false,
static: true,
width: 250,
}
@ -88,6 +65,7 @@ export const generateColumns = (): List<TableColumn> => {
disablePadding: false,
label: 'Balance',
custom: false,
static: true,
}
const actions: TableColumn = {
@ -105,6 +83,7 @@ export const generateColumns = (): List<TableColumn> => {
order: true,
label: 'Value',
custom: false,
static: true,
disablePadding: false,
}

View File

@ -3,18 +3,16 @@ import React, { useEffect, useState } from 'react'
import { useSelector } from 'react-redux'
import ReceiveModal from 'src/components/App/ReceiveModal'
import { Tokens } from './Tokens'
import { styles } from './style'
import Modal from 'src/components/Modal'
import ButtonLink from 'src/components/layout/ButtonLink'
import Col from 'src/components/layout/Col'
import Divider from 'src/components/layout/Divider'
import Row from 'src/components/layout/Row'
import { SAFELIST_ADDRESS } from 'src/routes/routes'
import SendModal from 'src/routes/safe/components/Balances/SendModal'
import CurrencyDropdown from 'src/routes/safe/components/CurrencyDropdown'
import { CurrencyDropdown } from 'src/routes/safe/components/CurrencyDropdown'
import {
safeFeaturesEnabledSelector,
safeNameSelector,
@ -35,7 +33,6 @@ export const BALANCE_ROW_TEST_ID = 'balance-row'
const INITIAL_STATE = {
erc721Enabled: false,
showToken: false,
showManageCollectibleModal: false,
sendFunds: {
isOpen: false,
selectedToken: '',
@ -95,17 +92,8 @@ const Balances = (): React.ReactElement => {
}))
}
const {
assetDivider,
assetTab,
assetTabActive,
assetTabs,
controls,
manageTokensButton,
receiveModal,
tokenControls,
} = classes
const { erc721Enabled, sendFunds, showManageCollectibleModal, showReceive, showToken } = state
const { assetDivider, assetTab, assetTabActive, assetTabs, controls, receiveModal, tokenControls } = classes
const { erc721Enabled, sendFunds, showReceive } = state
return (
<>
@ -140,32 +128,7 @@ const Balances = (): React.ReactElement => {
path={`${SAFELIST_ADDRESS}/${address}/balances/collectibles`}
exact
render={() => {
return !erc721Enabled ? (
<Redirect to={`${SAFELIST_ADDRESS}/${address}/balances`} />
) : (
<Col className={tokenControls} end="sm" sm={6} xs={12}>
<ButtonLink
className={manageTokensButton}
onClick={() => onShow('ManageCollectibleModal')}
size="lg"
testId="manage-tokens-btn"
>
Manage List
</ButtonLink>
<Modal
description={'Enable and disable tokens to be listed'}
handleClose={() => onHide('ManageCollectibleModal')}
open={showManageCollectibleModal}
title="Manage List"
>
<Tokens
modalScreen={'assetsList'}
onClose={() => onHide('ManageCollectibleModal')}
safeAddress={address}
/>
</Modal>
</Col>
)
return !erc721Enabled ? <Redirect to={`${SAFELIST_ADDRESS}/${address}/balances`} /> : null
}}
/>
<Route
@ -176,22 +139,6 @@ const Balances = (): React.ReactElement => {
<>
<Col className={tokenControls} end="sm" sm={6} xs={12}>
<CurrencyDropdown />
<ButtonLink
className={manageTokensButton}
onClick={() => onShow('Token')}
size="lg"
testId="manage-tokens-btn"
>
Manage List
</ButtonLink>
<Modal
description={'Enable and disable tokens to be listed'}
handleClose={() => onHide('Token')}
open={showToken}
title="Manage List"
>
<Tokens modalScreen={'tokenList'} onClose={() => onHide('Token')} safeAddress={address} />
</Modal>
</Col>
</>
)

View File

@ -13,26 +13,22 @@ import { useDispatch, useSelector } from 'react-redux'
import CheckIcon from './img/check.svg'
import { setSelectedCurrency } from 'src/logic/currencyValues/store/actions/setSelectedCurrency'
import { AVAILABLE_CURRENCIES } from 'src/logic/currencyValues/store/model/currencyValues'
import { currentCurrencySelector } from 'src/logic/currencyValues/store/selectors'
import { useDropdownStyles } from 'src/routes/safe/components/CurrencyDropdown/style'
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
import { availableCurrenciesSelector, currentCurrencySelector } from 'src/logic/currencyValues/store/selectors'
import { DropdownListTheme } from 'src/theme/mui'
import { setImageToPlaceholder } from '../Balances/utils'
import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils'
import Img from 'src/components/layout/Img/index'
import { getNetworkInfo } from 'src/config'
import { sameString } from 'src/utils/strings'
const { nativeCoin } = getNetworkInfo()
const CurrencyDropdown = (): React.ReactElement | null => {
const safeAddress = useSelector(safeParamAddressFromStateSelector) as string
export const CurrencyDropdown = (): React.ReactElement | null => {
const dispatch = useDispatch()
const [anchorEl, setAnchorEl] = useState(null)
const selectedCurrency = useSelector(currentCurrencySelector)
const [searchParams, setSearchParams] = useState('')
const currenciesList = Object.values(AVAILABLE_CURRENCIES)
const currenciesList = useSelector(availableCurrenciesSelector)
const tokenImage = nativeCoin.logoUri
const classes = useDropdownStyles({})
const currenciesListFiltered = currenciesList.filter((currency) =>
@ -47,8 +43,8 @@ const CurrencyDropdown = (): React.ReactElement | null => {
setAnchorEl(null)
}
const onCurrentCurrencyChangedHandler = (newCurrencySelectedName) => {
dispatch(setSelectedCurrency(safeAddress, newCurrencySelectedName))
const onCurrentCurrencyChangedHandler = (newCurrencySelectedName: string) => {
dispatch(setSelectedCurrency({ selectedCurrency: newCurrencySelectedName }))
handleClose()
}
@ -80,6 +76,7 @@ const CurrencyDropdown = (): React.ReactElement | null => {
horizontal: 'center',
vertical: 'top',
}}
TransitionProps={{ mountOnEnter: true, unmountOnExit: true }}
>
<MenuItem className={classes.listItemSearch} key="0">
<div className={classes.search}>
@ -139,5 +136,3 @@ const CurrencyDropdown = (): React.ReactElement | null => {
</MuiThemeProvider>
)
}
export default CurrencyDropdown

View File

@ -4,12 +4,11 @@ import { createSelector } from 'reselect'
import { Token } from 'src/logic/tokens/store/model/token'
import { tokensSelector } from 'src/logic/tokens/store/selectors'
import { getEthAsToken } from 'src/logic/tokens/utils/tokenHelpers'
import { isUserAnOwner } from 'src/logic/wallets/ethAddresses'
import { isUserAnOwner, sameAddress, ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
import { userAccountSelector } from 'src/logic/wallets/store/selectors'
import { safeActiveTokensSelector, safeBalancesSelector, safeSelector } from 'src/logic/safe/store/selectors'
import { SafeRecord } from 'src/logic/safe/store/models/safe'
// import { SPENDING_LIMIT_MODULE_ADDRESS } from 'src/utils/constants'
export const grantedSelector = createSelector(
userAccountSelector,
@ -37,15 +36,16 @@ export const extendedSafeTokensSelector = createSelector(
const tokenBalance = balances?.get(tokenAddress)
if (baseToken) {
map.set(tokenAddress, baseToken.set('balance', tokenBalance || '0'))
const updatedBaseToken = baseToken.set('balance', tokenBalance || { tokenBalance: '0', fiatBalance: '0' })
if (sameAddress(tokenAddress, ZERO_ADDRESS) || sameAddress(tokenAddress, ethAsToken?.address)) {
map.set(tokenAddress, updatedBaseToken.set('logoUri', ethAsToken?.logoUri || baseToken.logoUri))
} else {
map.set(tokenAddress, updatedBaseToken)
}
}
})
})
if (ethAsToken) {
return extendedTokens.set(ethAsToken.address, ethAsToken).toList()
}
return extendedTokens.toList()
},
)

View File

@ -9,12 +9,14 @@ export interface SafeReducerState {
defaultSafe: DefaultSafe
safes: SafesMap
latestMasterContractVersion: string
selectedCurrency: string
}
interface SafeReducerStateJSON {
defaultSafe: 'NOT_ASKED' | string | undefined
safes: Record<string, SafeRecordProps>
latestMasterContractVersion: string
selectedCurrency: string
}
export interface SafeReducerMap extends Map<string, any> {

View File

@ -13,11 +13,6 @@ import {
nftTokensReducer,
} from 'src/logic/collectibles/store/reducer/collectibles'
import cookies, { COOKIES_REDUCER_ID } from 'src/logic/cookies/store/reducer/cookies'
import currencyValuesStorageMiddleware from 'src/logic/currencyValues/store/middleware'
import currencyValues, {
CURRENCY_VALUES_KEY,
CurrencyValuesState,
} from 'src/logic/currencyValues/store/reducer/currencyValues'
import currentSession, {
CURRENT_SESSION_REDUCER_ID,
CurrentSessionState,
@ -30,11 +25,16 @@ import tokens, { TOKEN_REDUCER_ID, TokenState } from 'src/logic/tokens/store/red
import providerWatcher from 'src/logic/wallets/store/middlewares/providerWatcher'
import provider, { PROVIDER_REDUCER_ID, ProviderState } from 'src/logic/wallets/store/reducer/provider'
import notificationsMiddleware from 'src/logic/safe/store/middleware/notificationsMiddleware'
import safeStorage from 'src/logic/safe/store/middleware/safeStorage'
import { safeStorageMiddleware } from 'src/logic/safe/store/middleware/safeStorage'
import safe, { SAFE_REDUCER_ID } from 'src/logic/safe/store/reducer/safe'
import { NFTAssets, NFTTokens } from 'src/logic/collectibles/sources/collectibles.d'
import { SafeReducerMap } from 'src/routes/safe/store/reducer/types/safe'
import { AddressBookState } from 'src/logic/addressBook/model/addressBook'
import currencyValues, {
CURRENCY_VALUES_KEY,
CurrencyValuesState,
} from 'src/logic/currencyValues/store/reducer/currencyValues'
import { currencyValuesStorageMiddleware } from 'src/logic/currencyValues/store/middleware/currencyValuesStorageMiddleware'
export const history = createHashHistory()
@ -45,7 +45,7 @@ const finalCreateStore = composeEnhancers(
thunk,
routerMiddleware(history),
notificationsMiddleware,
safeStorage,
safeStorageMiddleware,
providerWatcher,
addressBookMiddleware,
currencyValuesStorageMiddleware,

View File

@ -1,9 +1,7 @@
//
import { fireEvent, waitForElement, act } from '@testing-library/react'
//
import { fireEvent, act } from '@testing-library/react'
import { MANAGE_TOKENS_BUTTON_TEST_ID } from 'src/routes/safe/components/Balances'
import { ADD_CUSTOM_TOKEN_BUTTON_TEST_ID } from 'src/routes/safe/components/Balances/Tokens/screens/TokenList'
import { TOGGLE_TOKEN_TEST_ID } from 'src/routes/safe/components/Balances/Tokens/screens/TokenList/TokenRow'
import { MANAGE_TOKENS_MODAL_CLOSE_BUTTON_TEST_ID } from 'src/routes/safe/components/Balances/Tokens'
export const clickOnManageTokens = (dom) => {
const btn = dom.getByTestId(MANAGE_TOKENS_BUTTON_TEST_ID)
@ -13,26 +11,3 @@ export const clickOnManageTokens = (dom) => {
})
}
export const clickOnAddCustomToken = (dom) => {
const btn = dom.getByTestId(ADD_CUSTOM_TOKEN_BUTTON_TEST_ID)
act(() => {
fireEvent.click(btn)
})
}
export const toggleToken = async (dom, symbol) => {
const btn = await waitForElement(() => dom.getByTestId(`${symbol}_${TOGGLE_TOKEN_TEST_ID}`))
act(() => {
fireEvent.click(btn)
})
}
export const closeManageTokensModal = (dom) => {
const btn = dom.getByTestId(MANAGE_TOKENS_MODAL_CLOSE_BUTTON_TEST_ID)
act(() => {
fireEvent.click(btn)
})
}

View File

@ -1,3 +1,2 @@
//
//
export * from './moveFunds.helper'
export * from './moveTokens.helper'

View File

@ -1,37 +0,0 @@
//
import * as React from 'react'
import { Map } from 'immutable'
import { checkMinedTx, checkPendingTx } from 'src/test/builder/safe.dom.utils'
import { makeToken, } from 'src/logic/tokens/store/model/token'
import addTokens from 'src/logic/tokens/store/actions/saveTokens'
export const dispatchAddTokenToList = async (store, tokenAddress) => {
const fetchTokensMock = jest.fn()
const tokens = Map().set(
'TKN',
makeToken({
address: tokenAddress,
name: 'OmiseGo',
symbol: 'OMG',
decimals: 18,
logoUri:
'https://github.com/TrustWallet/tokens/blob/master/images/0x6810e776880c02933d47db1b9fc05908e5386b96.png?raw=true',
}),
)
fetchTokensMock.mockImplementation(() => store.dispatch(addTokens(tokens)))
fetchTokensMock()
fetchTokensMock.mockRestore()
}
export const checkMinedMoveTokensTx = (Transaction, name) => {
checkMinedTx(Transaction, name)
}
export const checkPendingMoveTokensTx = async (
Transaction,
safeThreshold,
name,
statusses,
) => {
await checkPendingTx(Transaction, safeThreshold, name, statusses)
}