(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:
parent
1c74f39f37
commit
4ed181886b
|
@ -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'
|
||||
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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))
|
||||
},
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
|
@ -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,
|
||||
}),
|
||||
)
|
|
@ -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,
|
||||
}))
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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}`)
|
||||
})
|
||||
})
|
|
@ -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)
|
||||
}
|
|
@ -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])
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
|
@ -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> = {
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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>(() => {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 />}
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -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
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
|
@ -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
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
|
@ -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',
|
||||
},
|
||||
})
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
},
|
||||
)
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,3 +1,2 @@
|
|||
//
|
||||
//
|
||||
export * from './moveFunds.helper'
|
||||
export * from './moveTokens.helper'
|
|
@ -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)
|
||||
}
|
Loading…
Reference in New Issue