From 8a787076b73d447292b4fdaea8aa169306018a60 Mon Sep 17 00:00:00 2001 From: Josh Leonard <30185185+josheleonard@users.noreply.github.com> Date: Wed, 6 Dec 2023 17:01:32 +0000 Subject: [PATCH] chore: cleanup hidden and deleted tokens code --- .../common/actions/wallet_actions.ts | 5 - .../common/async/base-query-cache.ts | 203 ++++++++++++------ .../brave_wallet_ui/common/async/handlers.ts | 10 + .../brave_wallet_ui/common/async/lib.ts | 16 +- .../common/constants/local-storage-keys.ts | 22 +- .../common/hooks/assets-management.test.tsx | 158 +++++++------- .../common/hooks/assets-management.ts | 166 ++++++-------- .../common/selectors/wallet-selectors.ts | 8 - .../common/slices/api.slice.ts | 2 + .../slices/endpoints/token.endpoints.ts | 202 ++++++++--------- .../entities/blockchain-token.entity.ts | 19 +- .../common/slices/wallet.slice.ts | 51 ----- .../desktop/views/nfts/components/nfts.tsx | 26 ++- .../nft-grid-view/nft-grid-view-item.tsx | 25 ++- .../views/portfolio/portfolio-nft-asset.tsx | 21 +- components/brave_wallet_ui/constants/types.ts | 5 - .../stories/mock-data/mock-wallet-state.ts | 5 - .../brave_wallet_ui/utils/asset-utils.ts | 25 ++- 18 files changed, 508 insertions(+), 461 deletions(-) diff --git a/components/brave_wallet_ui/common/actions/wallet_actions.ts b/components/brave_wallet_ui/common/actions/wallet_actions.ts index 2396e22f94c2..368c062f27e0 100644 --- a/components/brave_wallet_ui/common/actions/wallet_actions.ts +++ b/components/brave_wallet_ui/common/actions/wallet_actions.ts @@ -48,12 +48,7 @@ export const { updateUserAsset, setHidePortfolioGraph, setHidePortfolioBalances, - setRemovedFungibleTokenIds, - setRemovedNonFungibleTokenIds, - setDeletedNonFungibleTokenIds, - setDeletedNonFungibleTokens, setHidePortfolioNFTsTab, - setRemovedNonFungibleTokens, setFilteredOutPortfolioNetworkKeys, setFilteredOutPortfolioAccountAddresses, setHidePortfolioSmallBalances, diff --git a/components/brave_wallet_ui/common/async/base-query-cache.ts b/components/brave_wallet_ui/common/async/base-query-cache.ts index b63d9bf58b00..b53fcbf9c8a1 100644 --- a/components/brave_wallet_ui/common/async/base-query-cache.ts +++ b/components/brave_wallet_ui/common/async/base-query-cache.ts @@ -43,7 +43,9 @@ import getAPIProxy from './bridge' import { addChainIdToToken, getAssetIdKey, - GetBlockchainTokenIdArg + GetBlockchainTokenIdArg, + getDeletedTokenIds, + getHiddenTokenIds } from '../../utils/asset-utils' import { addLogoToToken } from './lib' import { makeNetworkAsset } from '../../options/asset-options' @@ -77,6 +79,7 @@ export class BaseQueryCache { private _walletInfo?: BraveWallet.WalletInfo private _allAccountsInfo?: BraveWallet.AllAccountsInfo private _accountsRegistry?: AccountInfoEntityState + private _knownTokensRegistry?: BlockchainTokenEntityAdaptorState private _userTokensRegistry?: BlockchainTokenEntityAdaptorState private _nftImageIpfsGateWayUrlRegistry: Record = {} private _extractedIPFSUrlRegistry: Record = {} @@ -256,70 +259,38 @@ export class BaseQueryCache { this._networksRegistry = undefined } + getKnownTokensRegistry = async () => { + if (!this._knownTokensRegistry) { + const { blockchainRegistry } = apiProxyFetcher() + const networksRegistry = await this.getNetworksRegistry() + + this._knownTokensRegistry = await makeTokenRegistry({ + type: 'known', + networksRegistry, + blockchainRegistry + }) + } + return this._knownTokensRegistry + } + getUserTokensRegistry = async () => { if (!this._userTokensRegistry) { const { braveWalletService } = apiProxyFetcher() const networksRegistry = await this.getNetworksRegistry() - const tokenIdsByChainId: Record = {} - const tokenIdsByCoinType: Record = {} - const visibleTokenIds: string[] = [] - const visibleTokenIdsByChainId: Record = {} - const visibleTokenIdsByCoinType: Record = - {} - - const userTokenListsForNetworks = await mapLimit( - Object.entries(networksRegistry.entities), - 10, - async ([networkId, network]: [string, BraveWallet.NetworkInfo]) => { - if (!network) { - return [] - } - - const fullTokensListForNetwork: BraveWallet.BlockchainToken[] = - await fetchUserAssetsForNetwork(braveWalletService, network) - - tokenIdsByChainId[networkId] = - fullTokensListForNetwork.map(getAssetIdKey) - - tokenIdsByCoinType[network.coin] = ( - tokenIdsByCoinType[network.coin] || [] - ).concat(tokenIdsByChainId[networkId] || []) - - const visibleTokensForNetwork: BraveWallet.BlockchainToken[] = - fullTokensListForNetwork.filter((t) => t.visible) - - visibleTokenIdsByChainId[networkId] = - visibleTokensForNetwork.map(getAssetIdKey) - - visibleTokenIdsByCoinType[network.coin] = ( - visibleTokenIdsByCoinType[network.coin] || [] - ).concat(visibleTokenIdsByChainId[networkId] || []) - - visibleTokenIds.push(...visibleTokenIdsByChainId[networkId]) - - return fullTokensListForNetwork - } - ) - - const userTokensByChainIdRegistry = blockchainTokenEntityAdaptor.setAll( - { - ...blockchainTokenEntityAdaptorInitialState, - idsByChainId: tokenIdsByChainId, - tokenIdsByChainId, - visibleTokenIds, - visibleTokenIdsByChainId, - visibleTokenIdsByCoinType, - idsByCoinType: tokenIdsByCoinType - }, - userTokenListsForNetworks.flat(1) - ) - - this._userTokensRegistry = userTokensByChainIdRegistry + this._userTokensRegistry = await makeTokenRegistry({ + type: 'user', + networksRegistry, + braveWalletService + }) } return this._userTokensRegistry } + clearKnownTokensRegistry = () => { + this._knownTokensRegistry = undefined + } + clearUserTokensRegistry = () => { this._userTokensRegistry = undefined } @@ -416,15 +387,29 @@ export const resetCache = () => { } // internals -async function fetchUserAssetsForNetwork( - braveWalletService: BraveWallet.BraveWalletServiceRemote, - network: BraveWallet.NetworkInfo -) { +async function fetchAssetsForNetwork({ + assetListType, + blockchainRegistry, + braveWalletService, + network +}: + | { + assetListType: 'user' + braveWalletService: BraveWallet.BraveWalletServiceRemote + blockchainRegistry?: never + network: BraveWallet.NetworkInfo + } + | { + assetListType: 'known' + blockchainRegistry: BraveWallet.BlockchainRegistryRemote + braveWalletService?: never + network: BraveWallet.NetworkInfo + }) { // Get a list of user tokens for each coinType and network. - const { tokens } = await braveWalletService.getUserAssets( - network.chainId, - network.coin - ) + const { tokens } = + assetListType === 'user' + ? await braveWalletService.getUserAssets(network.chainId, network.coin) + : await blockchainRegistry.getAllTokens(network.chainId, network.coin) // Adds a logo and chainId to each token object const tokenList: BraveWallet.BlockchainToken[] = await mapLimit( @@ -446,3 +431,93 @@ async function fetchUserAssetsForNetwork( return tokenList } +async function makeTokenRegistry({ + braveWalletService, + blockchainRegistry, + networksRegistry, + type +}: + | { + type: 'user' + networksRegistry: NetworksRegistry + braveWalletService: BraveWallet.BraveWalletServiceRemote + blockchainRegistry?: never + } + | { + type: 'known' + networksRegistry: NetworksRegistry + blockchainRegistry: BraveWallet.BlockchainRegistryRemote + braveWalletService?: never + }) { + const tokenIdsByChainId: Record = {} + const tokenIdsByCoinType: Record = {} + const visibleTokenIds: string[] = [] + const visibleTokenIdsByChainId: Record = {} + const visibleTokenIdsByCoinType: Record = {} + + const deletedTokenIds: string[] = type === 'user' ? getDeletedTokenIds() : [] + const hiddenTokenIds: string[] = type === 'user' ? getHiddenTokenIds() : [] + const removedTokenIds = deletedTokenIds.concat(hiddenTokenIds) + + const tokenListsForNetworks = await mapLimit( + Object.entries(networksRegistry.entities), + 10, + async ([networkId, network]: [string, BraveWallet.NetworkInfo]) => { + if (!network) { + return [] + } + + const fullTokensListForNetwork: BraveWallet.BlockchainToken[] = + type === 'known' + ? await fetchAssetsForNetwork({ + assetListType: type, + blockchainRegistry, + network + }) + : await fetchAssetsForNetwork({ + assetListType: type, + braveWalletService, + network + }) + + tokenIdsByChainId[networkId] = fullTokensListForNetwork.map(getAssetIdKey) + + tokenIdsByCoinType[network.coin] = ( + tokenIdsByCoinType[network.coin] || [] + ).concat(tokenIdsByChainId[networkId] || []) + + const visibleTokensForNetwork: BraveWallet.BlockchainToken[] = + fullTokensListForNetwork.filter((t) => + t.visible && type === 'user' + ? !removedTokenIds.includes(getAssetIdKey(t)) + : true + ) + + visibleTokenIdsByChainId[networkId] = + visibleTokensForNetwork.map(getAssetIdKey) + + visibleTokenIdsByCoinType[network.coin] = ( + visibleTokenIdsByCoinType[network.coin] || [] + ).concat(visibleTokenIdsByChainId[networkId] || []) + + visibleTokenIds.push(...visibleTokenIdsByChainId[networkId]) + + return fullTokensListForNetwork + } + ) + + return blockchainTokenEntityAdaptor.setAll( + { + ...blockchainTokenEntityAdaptorInitialState, + idsByChainId: tokenIdsByChainId, + tokenIdsByChainId, + visibleTokenIds, + visibleTokenIdsByChainId, + visibleTokenIdsByCoinType, + idsByCoinType: tokenIdsByCoinType, + deletedTokenIds, + hiddenTokenIds + }, + tokenListsForNetworks.flat(1) + ) +} diff --git a/components/brave_wallet_ui/common/async/handlers.ts b/components/brave_wallet_ui/common/async/handlers.ts index 3dfce51bfffd..1a9afca67d0d 100644 --- a/components/brave_wallet_ui/common/async/handlers.ts +++ b/components/brave_wallet_ui/common/async/handlers.ts @@ -25,6 +25,8 @@ import { Store } from './types' import InteractionNotifier from './interactionNotifier' import { walletApi } from '../slices/api.slice' import { getVisibleNetworksList } from '../../utils/api-utils' +import { getAssetIdKey, getDeletedTokenIds } from '../../utils/asset-utils' +import { LOCAL_STORAGE_KEYS } from '../constants/local-storage-keys' const handler = new AsyncActionHandler() @@ -211,6 +213,14 @@ handler.on( const result = await braveWalletService.addUserAsset(payload) + // token may have previously been deleted + localStorage.setItem( + LOCAL_STORAGE_KEYS.USER_DELETED_TOKEN_IDS, + JSON.stringify( + getDeletedTokenIds().filter((id) => id !== getAssetIdKey(payload)) + ) + ) + // Refresh balances here for adding ERC721 tokens if result is successful if ((payload.isErc721 || payload.isNft) && result.success) { refreshBalancesPricesAndHistory(store) diff --git a/components/brave_wallet_ui/common/async/lib.ts b/components/brave_wallet_ui/common/async/lib.ts index 1d9bb4a04f9c..e4389c71bc03 100644 --- a/components/brave_wallet_ui/common/async/lib.ts +++ b/components/brave_wallet_ui/common/async/lib.ts @@ -15,7 +15,11 @@ import { import * as WalletActions from '../actions/wallet_actions' // Utils -import { getAssetIdKey, isNativeAsset } from '../../utils/asset-utils' +import { + getAssetIdKey, + getHiddenOrDeletedTokenIdsList, + isNativeAsset +} from '../../utils/asset-utils' import { makeNativeAssetLogo, makeNetworkAsset @@ -210,19 +214,11 @@ export function refreshVisibleTokenInfo( async (item: BraveWallet.NetworkInfo) => await inner(item) ) - const removedAssetIds = [ - ...getState().wallet.removedFungibleTokenIds, - ...getState().wallet.removedNonFungibleTokenIds, - ...getState().wallet.deletedNonFungibleTokenIds - ] + const removedAssetIds = getHiddenOrDeletedTokenIdsList() const userVisibleTokensInfo = visibleAssets .flat(1) .filter((token) => !removedAssetIds.includes(getAssetIdKey(token))) - const removedNfts = visibleAssets - .flat(1) - .filter((token) => removedAssetIds.includes(getAssetIdKey(token))) await dispatch(WalletActions.setVisibleTokensInfo(userVisibleTokensInfo)) - await dispatch(WalletActions.setRemovedNonFungibleTokens(removedNfts)) } } diff --git a/components/brave_wallet_ui/common/constants/local-storage-keys.ts b/components/brave_wallet_ui/common/constants/local-storage-keys.ts index b0ceecb4042d..7aa366b059e6 100644 --- a/components/brave_wallet_ui/common/constants/local-storage-keys.ts +++ b/components/brave_wallet_ui/common/constants/local-storage-keys.ts @@ -13,12 +13,8 @@ export const LOCAL_STORAGE_KEYS = { IS_IPFS_BANNER_HIDDEN: 'BRAVE_WALLET_IS_IPFS_BANNER_HIDDEN', IS_ENABLE_NFT_AUTO_DISCOVERY_MODAL_HIDDEN: 'BRAVE_WALLET_IS_ENABLE_NFT_AUTO_DISCOVERY_MODAL_HIDDEN', - USER_REMOVED_NON_FUNGIBLE_TOKEN_IDS: - 'BRAVE_WALLET_USER_REMOVED_NON_FUNGIBLE_TOKEN_IDS', - USER_DELETED_NON_FUNGIBLE_TOKEN_IDS: - 'BRAVE_WALLET_USER_DELETED_NON_FUNGIBLE_TOKEN_IDS', - USER_REMOVED_FUNGIBLE_TOKEN_IDS: - 'BRAVE_WALLET_USER_REMOVED_FUNGIBLE_TOKEN_IDS', + USER_HIDDEN_TOKEN_IDS: 'BRAVE_WALLET_USER_HIDDEN_TOKEN_IDS', + USER_DELETED_TOKEN_IDS: 'BRAVE_WALLET_USER_DELETED_TOKEN_IDS', DEBUG: 'BRAVE_WALLET_DEBUG', FILTERED_OUT_PORTFOLIO_NETWORK_KEYS: 'BRAVE_WALLET_FILTERED_OUT_PORTFOLIO_NETWORK_KEYS', @@ -37,11 +33,17 @@ export const LOCAL_STORAGE_KEYS = { } as const const LOCAL_STORAGE_KEYS_DEPRECATED = { - PORTFOLIO_ACCOUNT_FILTER_OPTION: 'PORTFOLIO_ACCOUNT_FILTER_OPTION' + PORTFOLIO_ACCOUNT_FILTER_OPTION: 'PORTFOLIO_ACCOUNT_FILTER_OPTION', + USER_REMOVED_NON_FUNGIBLE_TOKEN_IDS: + 'BRAVE_WALLET_USER_REMOVED_NON_FUNGIBLE_TOKEN_IDS', + USER_DELETED_NON_FUNGIBLE_TOKEN_IDS: + 'BRAVE_WALLET_USER_DELETED_NON_FUNGIBLE_TOKEN_IDS', + USER_REMOVED_FUNGIBLE_TOKEN_IDS: + 'BRAVE_WALLET_USER_REMOVED_FUNGIBLE_TOKEN_IDS' } export const removeDeprecatedLocalStorageKeys = () => { - window.localStorage.removeItem( - LOCAL_STORAGE_KEYS_DEPRECATED.PORTFOLIO_ACCOUNT_FILTER_OPTION - ) + Object.keys(LOCAL_STORAGE_KEYS_DEPRECATED).forEach((key) => { + window.localStorage.removeItem(LOCAL_STORAGE_KEYS_DEPRECATED[key]) + }) } diff --git a/components/brave_wallet_ui/common/hooks/assets-management.test.tsx b/components/brave_wallet_ui/common/hooks/assets-management.test.tsx index ee40c2b42b98..782dfcfc3beb 100644 --- a/components/brave_wallet_ui/common/hooks/assets-management.test.tsx +++ b/components/brave_wallet_ui/common/hooks/assets-management.test.tsx @@ -7,9 +7,6 @@ import { renderHook, act } from '@testing-library/react-hooks' // Redux import { Provider } from 'react-redux' -import { combineReducers, createStore } from 'redux' -import { createWalletReducer } from '../slices/wallet.slice' -import { createPageReducer } from '../../page/reducers/page_reducer' // Constants import { BraveWallet } from '../../constants/types' @@ -28,6 +25,7 @@ import * as MockedLib from '../async/__mocks__/lib' import { mockWalletState } from '../../stories/mock-data/mock-wallet-state' import { mockPageState } from '../../stories/mock-data/mock-page-state' import { LibContext } from '../context/lib.context' +import { createMockStore } from '../../utils/test-utils' const mockCustomToken = { contractAddress: 'customTokenContractAddress', @@ -43,15 +41,13 @@ const mockCustomToken = { chainId: '0x1' } as BraveWallet.BlockchainToken -const makeStore = (customStore?: any) => { +const makeStore = (customStore?: ReturnType) => { const store = customStore || - createStore( - combineReducers({ - wallet: createWalletReducer(mockWalletState), - page: createPageReducer(mockPageState) - }) - ) + createMockStore({ + walletStateOverride: mockWalletState, + pageStateOverride: mockPageState + }) store.dispatch = jest.fn(store.dispatch) return store @@ -78,11 +74,12 @@ describe('useAssetManagement hook', () => { const { wallet: { userVisibleTokensInfo } } = store.getState() - act(() => - result.current.onUpdateVisibleAssets([ - ...userVisibleTokensInfo, - mockCustomToken - ]) + await act( + async () => + await result.current.onUpdateVisibleAssets([ + ...userVisibleTokensInfo, + mockCustomToken + ]) ) expect(store.dispatch).toHaveBeenCalledWith( @@ -94,7 +91,7 @@ describe('useAssetManagement hook', () => { ) }) - it('should remove an asset', () => { + it('should remove an asset', async () => { const store = makeStore() const renderHookOptions = renderHookOptionsWithCustomStore(store) const { result } = renderHook(() => useAssetManagement(), renderHookOptions) @@ -102,7 +99,7 @@ describe('useAssetManagement hook', () => { const { wallet: { userVisibleTokensInfo } } = store.getState() - act(() => + await act(async () => result.current.onUpdateVisibleAssets(userVisibleTokensInfo.slice(1)) ) @@ -115,7 +112,7 @@ describe('useAssetManagement hook', () => { ) }) - it('should remove and add an asset', () => { + it('should remove and add an asset', async () => { const store = makeStore() const renderHookOptions = renderHookOptionsWithCustomStore(store) const { result } = renderHook(() => useAssetManagement(), renderHookOptions) @@ -123,11 +120,12 @@ describe('useAssetManagement hook', () => { const { wallet: { userVisibleTokensInfo } } = store.getState() - act(() => - result.current.onUpdateVisibleAssets([ - ...userVisibleTokensInfo.slice(1), - mockCustomToken - ]) + await act( + async () => + await result.current.onUpdateVisibleAssets([ + ...userVisibleTokensInfo.slice(1), + mockCustomToken + ]) ) expect(store.dispatch).toHaveBeenCalledWith( @@ -143,19 +141,17 @@ describe('useAssetManagement hook', () => { ) }) - it('should set custom tokens visibility to false', () => { - const customStore = createStore( - combineReducers({ - wallet: createWalletReducer({ - ...mockWalletState, - userVisibleTokensInfo: [ - mockCustomToken, - ...mockWalletState.userVisibleTokensInfo - ] - }), - page: createPageReducer(mockPageState) - }) - ) + it('should set custom tokens visibility to false', async () => { + const customStore = createMockStore({ + walletStateOverride: { + ...mockWalletState, + userVisibleTokensInfo: [ + mockCustomToken, + ...mockWalletState.userVisibleTokensInfo + ] + }, + pageStateOverride: mockPageState + }) const store = makeStore(customStore) const renderHookOptions = renderHookOptionsWithCustomStore(store) const { result } = renderHook(() => useAssetManagement(), renderHookOptions) @@ -163,11 +159,12 @@ describe('useAssetManagement hook', () => { const { wallet: { userVisibleTokensInfo } } = store.getState() - act(() => - result.current.onUpdateVisibleAssets([ - { ...mockCustomToken, visible: false }, - ...userVisibleTokensInfo.slice(1) - ]) + await act( + async () => + await result.current.onUpdateVisibleAssets([ + { ...mockCustomToken, visible: false }, + ...userVisibleTokensInfo.slice(1) + ]) ) expect(store.dispatch).toHaveBeenCalledWith( @@ -182,28 +179,27 @@ describe('useAssetManagement hook', () => { ) }) - it('should set custom tokens visibility to true', () => { - const customStore = createStore( - combineReducers({ - wallet: createWalletReducer({ - ...mockWalletState, - userVisibleTokensInfo: [ - { ...mockCustomToken, visible: false }, - ...mockWalletState.userVisibleTokensInfo - ] - }), - page: createPageReducer(mockPageState) - }) - ) + it('should set custom tokens visibility to true', async () => { + const customStore = createMockStore({ + walletStateOverride: { + ...mockWalletState, + userVisibleTokensInfo: [ + { ...mockCustomToken, visible: false }, + ...mockWalletState.userVisibleTokensInfo + ] + }, + pageStateOverride: mockPageState + }) const store = makeStore(customStore) const renderHookOptions = renderHookOptionsWithCustomStore(store) const { result } = renderHook(() => useAssetManagement(), renderHookOptions) - act(() => - result.current.onUpdateVisibleAssets([ - mockCustomToken, - ...mockWalletState.userVisibleTokensInfo - ]) + await act( + async () => + await result.current.onUpdateVisibleAssets([ + mockCustomToken, + ...mockWalletState.userVisibleTokensInfo + ]) ) expect(store.dispatch).toHaveBeenCalledWith( WalletActions.setUserAssetVisible({ @@ -216,12 +212,14 @@ describe('useAssetManagement hook', () => { ) }) - it('should add token to visible assets list if not found', () => { + it('should add token to visible assets list if not found', async () => { const store = makeStore() const renderHookOptions = renderHookOptionsWithCustomStore(store) const { result } = renderHook(() => useAssetManagement(), renderHookOptions) - act(() => result.current.makeTokenVisible(mockCustomToken)) + await act( + async () => await result.current.makeTokenVisible(mockCustomToken) + ) expect(store.dispatch).toHaveBeenCalledWith( WalletActions.addUserAsset({ ...mockCustomToken, logo: '' }) @@ -231,7 +229,7 @@ describe('useAssetManagement hook', () => { ) }) - it('should not add token to visible list if already there', () => { + it('should not add token to visible list if already there', async () => { const store = makeStore() const renderHookOptions = renderHookOptionsWithCustomStore(store) const { result } = renderHook(() => useAssetManagement(), renderHookOptions) @@ -239,29 +237,41 @@ describe('useAssetManagement hook', () => { const { wallet: { userVisibleTokensInfo } } = store.getState() - act(() => result.current.makeTokenVisible(userVisibleTokensInfo[0])) - expect(store.dispatch).not.toHaveBeenCalled() + // useGetUserTokensRegistryQuery call + expect(store.dispatch).toHaveBeenCalledTimes(1) + + await act( + async () => + await result.current.makeTokenVisible(userVisibleTokensInfo[0]) + ) + + // No additional dispatches + expect(store.dispatch).toHaveBeenCalledTimes(1) }) - it('should update visibility of token if not visible already', () => { - const customStore = createStore( - combineReducers({ - wallet: createWalletReducer({ + it('should update visibility of token if not visible already', async () => { + const store = makeStore( + createMockStore({ + walletStateOverride: { ...mockWalletState, userVisibleTokensInfo: [ - { ...mockCustomToken, visible: false }, - ...mockWalletState.userVisibleTokensInfo + ...mockWalletState.userVisibleTokensInfo, + { ...mockCustomToken, visible: false } ] - }), - page: createPageReducer(mockPageState) + }, + pageStateOverride: mockPageState }) ) - const store = makeStore(customStore) const renderHookOptions = renderHookOptionsWithCustomStore(store) const { result } = renderHook(() => useAssetManagement(), renderHookOptions) - act(() => result.current.makeTokenVisible(mockCustomToken)) + // useGetUserTokensRegistryQuery call + expect(store.dispatch).toHaveBeenCalledTimes(1) + + await act( + async () => await result.current.makeTokenVisible(mockCustomToken) + ) expect(store.dispatch).toHaveBeenCalledWith( WalletActions.setUserAssetVisible({ diff --git a/components/brave_wallet_ui/common/hooks/assets-management.ts b/components/brave_wallet_ui/common/hooks/assets-management.ts index db964a4a2975..ae727992e7bc 100644 --- a/components/brave_wallet_ui/common/hooks/assets-management.ts +++ b/components/brave_wallet_ui/common/hooks/assets-management.ts @@ -5,6 +5,7 @@ import * as React from 'react' import { useDispatch } from 'react-redux' +import { eachLimit } from 'async' // Selectors import { useUnsafeWalletSelector } from './use-safe-selector' @@ -16,8 +17,15 @@ import { BraveWallet } from '../../constants/types' // Utils import { stripERC20TokenImageURL } from '../../utils/string-utils' import { WalletActions } from '../actions' -import { LOCAL_STORAGE_KEYS } from '../constants/local-storage-keys' -import { getAssetIdKey } from '../../utils/asset-utils' +import { + getAssetIdKey, + isTokenRemovedInLocalStorage +} from '../../utils/asset-utils' +import { + useGetUserTokensRegistryQuery, + useHideOrDeleteTokenMutation, + useRestoreHiddenTokenMutation +} from '../slices/api.slice' const onlyInLeft = ( left: BraveWallet.BlockchainToken[], @@ -54,88 +62,35 @@ export function useAssetManagement() { const userVisibleTokensInfo = useUnsafeWalletSelector( WalletSelectors.userVisibleTokensInfo ) - const removedFungibleTokenIds = useUnsafeWalletSelector( - WalletSelectors.removedFungibleTokenIds - ) - const removedNonFungibleTokenIds = useUnsafeWalletSelector( - WalletSelectors.removedNonFungibleTokenIds - ) - const deletedNonFungibleTokenIds = useUnsafeWalletSelector( - WalletSelectors.deletedNonFungibleTokenIds - ) // redux const dispatch = useDispatch() - const tokenIsSetAsRemovedInLocalStorage = React.useCallback( - (token: BraveWallet.BlockchainToken) => { - const assetId = getAssetIdKey(token) - return token.isNft || token.isErc1155 || token.isErc721 - ? removedNonFungibleTokenIds.includes(assetId) - : removedFungibleTokenIds.includes(assetId) - }, - [removedNonFungibleTokenIds, removedFungibleTokenIds] - ) + // queries + const { data: userTokensRegistry } = useGetUserTokensRegistryQuery() - const addOrRemoveTokenInLocalStorage = React.useCallback( - (token: BraveWallet.BlockchainToken, addOrRemove: 'add' | 'remove') => { - const assetId = getAssetIdKey(token) - const isNFT = token.isNft || token.isErc1155 || token.isErc721 - const removedList = isNFT - ? removedNonFungibleTokenIds - : removedFungibleTokenIds - const localStorageKey = isNFT - ? LOCAL_STORAGE_KEYS.USER_REMOVED_NON_FUNGIBLE_TOKEN_IDS - : LOCAL_STORAGE_KEYS.USER_REMOVED_FUNGIBLE_TOKEN_IDS - - // add assetId if it is not in the array - if (addOrRemove === 'remove') { - const newList = [...removedList, assetId] - // update state - if (isNFT) { - dispatch(WalletActions.setRemovedNonFungibleTokenIds(newList)) - } else { - dispatch(WalletActions.setRemovedFungibleTokenIds(newList)) - } - // persist array - localStorage.setItem(localStorageKey, JSON.stringify(newList)) - } - - // add assetId if it is not in the array - if (addOrRemove === 'add') { - const newList = removedList.filter((id) => id !== assetId) - // update state - if (isNFT) { - dispatch(WalletActions.setRemovedNonFungibleTokenIds(newList)) - } else { - dispatch(WalletActions.setRemovedFungibleTokenIds(newList)) - } - // persist array - localStorage.setItem(localStorageKey, JSON.stringify(newList)) - } - }, - [removedNonFungibleTokenIds, removedFungibleTokenIds] - ) + // mutations + const [hideOrDeleteToken] = useHideOrDeleteTokenMutation() + const [restoreHiddenToken] = useRestoreHiddenTokenMutation() const addNftToDeletedNftsList = React.useCallback( - (token: BraveWallet.BlockchainToken) => { - const assetId = getAssetIdKey(token) - const newList = [...deletedNonFungibleTokenIds, assetId] - // update state - dispatch(WalletActions.setDeletedNonFungibleTokenIds(newList)) - // persist array - localStorage.setItem( - LOCAL_STORAGE_KEYS.USER_DELETED_NON_FUNGIBLE_TOKEN_IDS, - JSON.stringify(newList) - ) + async (token: BraveWallet.BlockchainToken) => { + await hideOrDeleteToken({ + mode: 'delete', + tokenId: getAssetIdKey(token) + }) }, - [deletedNonFungibleTokenIds] + [hideOrDeleteToken] ) const onAddUserAsset = React.useCallback( - (token: BraveWallet.BlockchainToken) => { - if (tokenIsSetAsRemovedInLocalStorage(token)) { - addOrRemoveTokenInLocalStorage(token, 'add') + async (token: BraveWallet.BlockchainToken) => { + const assetId = getAssetIdKey(token) + if ( + userTokensRegistry && + isTokenRemovedInLocalStorage(assetId, userTokensRegistry.hiddenTokenIds) + ) { + await restoreHiddenToken(assetId) } else { dispatch( WalletActions.addUserAsset({ @@ -145,12 +100,12 @@ export function useAssetManagement() { ) } }, - [addOrRemoveTokenInLocalStorage, tokenIsSetAsRemovedInLocalStorage] + [restoreHiddenToken, userTokensRegistry] ) const onAddCustomAsset = React.useCallback( - (token: BraveWallet.BlockchainToken) => { - onAddUserAsset(token) + async (token: BraveWallet.BlockchainToken) => { + await onAddUserAsset(token) // We handle refreshing balances for ERC721 tokens in the // addUserAsset handler. @@ -162,42 +117,61 @@ export function useAssetManagement() { ) const onUpdateVisibleAssets = React.useCallback( - (updatedTokensList: BraveWallet.BlockchainToken[]) => { + async (updatedTokensList: BraveWallet.BlockchainToken[]) => { // Gets a list of all added tokens and adds them to the // userVisibleTokensInfo list - onlyInLeft(updatedTokensList, userVisibleTokensInfo).forEach((token) => - onAddUserAsset(token) + await eachLimit( + onlyInLeft(updatedTokensList, userVisibleTokensInfo), + 10, + onAddUserAsset ) // Gets a list of all removed tokens and removes them from the // userVisibleTokensInfo list - onlyInLeft(userVisibleTokensInfo, updatedTokensList).forEach((token) => { - dispatch(WalletActions.removeUserAsset(token)) - if (!tokenIsSetAsRemovedInLocalStorage(token)) { - addOrRemoveTokenInLocalStorage(token, 'remove') + await eachLimit( + onlyInLeft(userVisibleTokensInfo, updatedTokensList), + 10, + async (token) => { + dispatch(WalletActions.removeUserAsset(token)) + const tokenId = getAssetIdKey(token) + if ( + userTokensRegistry && + !isTokenRemovedInLocalStorage( + tokenId, + userTokensRegistry.hiddenTokenIds + ) + ) { + await hideOrDeleteToken({ mode: 'hide', tokenId }) + } } - }) + ) // Gets a list of custom tokens and native assets returned from // updatedTokensList payload then compares against userVisibleTokensInfo // list and updates the tokens visibility if it has changed. - findTokensWithMismatchedVisibility( - updatedTokensList, - userVisibleTokensInfo - ).forEach((token) => - dispatch( - WalletActions.setUserAssetVisible({ token, isVisible: token.visible }) - ) + await eachLimit( + findTokensWithMismatchedVisibility( + updatedTokensList, + userVisibleTokensInfo + ), + 10, + async (token) => + await dispatch( + WalletActions.setUserAssetVisible({ + token, + isVisible: token.visible + }) + ) ) // Refresh Balances, Prices and Price History when done. dispatch(WalletActions.refreshBalancesAndPriceHistory()) }, - [userVisibleTokensInfo, addOrRemoveTokenInLocalStorage] + [userVisibleTokensInfo, hideOrDeleteToken] ) const makeTokenVisible = React.useCallback( - (token: BraveWallet.BlockchainToken) => { + async (token: BraveWallet.BlockchainToken) => { const foundTokenIdx = userVisibleTokensInfo.findIndex( (t) => t.contractAddress.toLowerCase() === @@ -208,7 +182,8 @@ export function useAssetManagement() { // If token is not part of user-visible tokens, add it. if (foundTokenIdx === -1) { - return onUpdateVisibleAssets([...updatedTokensList, token]) + await onUpdateVisibleAssets([...updatedTokensList, token]) + return } if (userVisibleTokensInfo[foundTokenIdx].visible) { @@ -219,7 +194,7 @@ export function useAssetManagement() { // - toggle visibility for custom tokens // - do nothing for non-custom tokens updatedTokensList.splice(foundTokenIdx, 1, { ...token, visible: true }) - onUpdateVisibleAssets(updatedTokensList) + await onUpdateVisibleAssets(updatedTokensList) }, [userVisibleTokensInfo, onUpdateVisibleAssets] ) @@ -228,7 +203,6 @@ export function useAssetManagement() { onUpdateVisibleAssets, onAddCustomAsset, makeTokenVisible, - addOrRemoveTokenInLocalStorage, addNftToDeletedNftsList } } diff --git a/components/brave_wallet_ui/common/selectors/wallet-selectors.ts b/components/brave_wallet_ui/common/selectors/wallet-selectors.ts index b43c430b062a..ff0990bb06a1 100644 --- a/components/brave_wallet_ui/common/selectors/wallet-selectors.ts +++ b/components/brave_wallet_ui/common/selectors/wallet-selectors.ts @@ -31,8 +31,6 @@ export const hidePortfolioBalances = ({ wallet }: State) => wallet.hidePortfolioBalances export const hidePortfolioNFTsTab = ({ wallet }: State) => wallet.hidePortfolioNFTsTab -export const removedNonFungibleTokens = ({ wallet }: State) => - wallet.removedNonFungibleTokens export const filteredOutPortfolioNetworkKeys = ({ wallet }: State) => wallet.filteredOutPortfolioNetworkKeys export const filteredOutPortfolioAccountAddresses = ({ wallet }: State) => @@ -64,11 +62,5 @@ export const userVisibleTokensInfo = ({ wallet }: State) => wallet.userVisibleTokensInfo export const selectedAccountFilter = ({ wallet }: State) => wallet.selectedAccountFilter -export const removedFungibleTokenIds = ({ wallet }: State) => - wallet.removedFungibleTokenIds -export const removedNonFungibleTokenIds = ({ wallet }: State) => - wallet.removedNonFungibleTokenIds -export const deletedNonFungibleTokenIds = ({ wallet }: State) => - wallet.deletedNonFungibleTokenIds export const isRefreshingNetworksAndTokens = ({ wallet }: State) => wallet.isRefreshingNetworksAndTokens diff --git a/components/brave_wallet_ui/common/slices/api.slice.ts b/components/brave_wallet_ui/common/slices/api.slice.ts index 5b600718ee34..ccacda1f38d1 100644 --- a/components/brave_wallet_ui/common/slices/api.slice.ts +++ b/components/brave_wallet_ui/common/slices/api.slice.ts @@ -228,6 +228,7 @@ export const { useGetUserTokensRegistryQuery, useGetWalletsToImportQuery, useHideNetworksMutation, + useHideOrDeleteTokenMutation, useImportAccountFromJsonMutation, useImportAccountMutation, useImportFromCryptoWalletsMutation, @@ -276,6 +277,7 @@ export const { useReportActiveWalletsToP3AMutation, useReportOnboardingActionMutation, useRequestSitePermissionMutation, + useRestoreHiddenTokenMutation, useRestoreNetworksMutation, useRestoreWalletMutation, useRetryTransactionMutation, diff --git a/components/brave_wallet_ui/common/slices/endpoints/token.endpoints.ts b/components/brave_wallet_ui/common/slices/endpoints/token.endpoints.ts index fecaaf5a0ab3..c8ea5b99b1d9 100644 --- a/components/brave_wallet_ui/common/slices/endpoints/token.endpoints.ts +++ b/components/brave_wallet_ui/common/slices/endpoints/token.endpoints.ts @@ -4,7 +4,6 @@ // You can obtain one at https://mozilla.org/MPL/2.0/. import { EntityId } from '@reduxjs/toolkit' -import { mapLimit } from 'async' // types import { BraveWallet } from '../../../constants/types' @@ -14,16 +13,17 @@ import type { BaseQueryCache } from '../../async/base-query-cache' // utils import { handleEndpointError } from '../../../utils/api-utils' -import { addChainIdToToken, getAssetIdKey } from '../../../utils/asset-utils' -import { getEntitiesListFromEntityState } from '../../../utils/entities.utils' +import { + getAssetIdKey, + getDeletedTokenIds, + getHiddenTokenIds +} from '../../../utils/asset-utils' import { cacher } from '../../../utils/query-cache-utils' -import { addLogoToToken } from '../../async/lib' import { BlockchainTokenEntityAdaptorState, - blockchainTokenEntityAdaptor, - blockchainTokenEntityAdaptorInitialState + blockchainTokenEntityAdaptor } from '../entities/blockchain-token.entity' -import { networkEntityAdapter } from '../entities/network.entity' +import { LOCAL_STORAGE_KEYS } from '../../constants/local-storage-keys' export const TOKEN_TAG_IDS = { REGISTRY: 'REGISTRY' @@ -37,97 +37,9 @@ export const tokenEndpoints = ({ getTokensRegistry: query({ queryFn: async (arg, { endpoint }, extraOptions, baseQuery) => { try { - const { - cache, - data: { blockchainRegistry } - } = baseQuery(undefined) - - const networksRegistry = await cache.getNetworksRegistry() - const networksList: BraveWallet.NetworkInfo[] = - getEntitiesListFromEntityState( - networksRegistry, - networksRegistry.visibleIds - ) - - const tokenIdsByChainId: Record = {} - const tokenIdsByCoinType: Record = {} - - const getTokensList = async () => { - const tokenListsForNetworks = await mapLimit( - networksList, - 10, - async (network: BraveWallet.NetworkInfo) => { - const networkId = networkEntityAdapter.selectId(network) - - const { tokens } = await blockchainRegistry.getAllTokens( - network.chainId, - network.coin - ) - - const fullTokensListForChain: // - BraveWallet.BlockchainToken[] = await mapLimit( - tokens, - 10, - async (token: BraveWallet.BlockchainToken) => { - return addChainIdToToken( - await addLogoToToken(token), - network.chainId - ) - } - ) - - tokenIdsByChainId[networkId] = - fullTokensListForChain.map(getAssetIdKey) - - tokenIdsByCoinType[network.coin] = ( - tokenIdsByCoinType[network.coin] || [] - ).concat(tokenIdsByChainId[networkId] || []) - - return fullTokensListForChain - } - ) - - const flattenedTokensList = tokenListsForNetworks.flat(1) - return flattenedTokensList - } - - let flattenedTokensList = await getTokensList() - - // on startup, the tokens list returned from core may be empty - const startDate = new Date() - const timeoutSeconds = 5 - const timeoutMilliseconds = timeoutSeconds * 1000 - - // retry until we have some tokens or the request takes too - // long - while ( - // empty list - flattenedTokensList.length < 1 && - // try until timeout reached - new Date().getTime() - startDate.getTime() < timeoutMilliseconds - ) { - flattenedTokensList = await getTokensList() - } - - // return an error on timeout, so a retry can be attempted - if (flattenedTokensList.length === 0) { - throw new Error('No tokens found in tokens registry') - } - - const tokensByChainIdRegistry = blockchainTokenEntityAdaptor.setAll( - { - ...blockchainTokenEntityAdaptorInitialState, - idsByChainId: tokenIdsByChainId, - idsByCoinType: tokenIdsByCoinType, - visibleTokenIds: [], - visibleTokenIdsByChainId: {}, - visibleTokenIdsByCoinType: {} - }, - flattenedTokensList - ) - + const { cache } = baseQuery(undefined) return { - data: tokensByChainIdRegistry + data: await cache.getKnownTokensRegistry() } } catch (error) { return handleEndpointError( @@ -139,6 +51,7 @@ export const tokenEndpoints = ({ }, providesTags: cacher.providesRegistry('KnownBlockchainTokens') }), + getUserTokensRegistry: query({ queryFn: async (arg, { endpoint }, extraOptions, baseQuery) => { try { @@ -163,6 +76,7 @@ export const tokenEndpoints = ({ } ] }), + addUserToken: mutation<{ id: EntityId }, BraveWallet.BlockchainToken>({ queryFn: async (tokenArg, { dispatch }, extraOptions, baseQuery) => { const { @@ -183,6 +97,14 @@ export const tokenEndpoints = ({ } } + // token may have previously been deleted + localStorage.setItem( + LOCAL_STORAGE_KEYS.USER_DELETED_TOKEN_IDS, + JSON.stringify( + getDeletedTokenIds().filter((id) => id !== tokenIdentifier) + ) + ) + return { data: { id: tokenIdentifier } } @@ -192,6 +114,7 @@ export const tokenEndpoints = ({ { type: 'UserBlockchainTokens', id: getAssetIdKey(tokenArg) } ] }), + removeUserToken: mutation({ queryFn: async (tokenArg, { endpoint }, extraOptions, baseQuery) => { const { @@ -224,6 +147,7 @@ export const tokenEndpoints = ({ { type: 'UserBlockchainTokens', id: getAssetIdKey(tokenArg) } ] }), + updateUserToken: mutation<{ id: EntityId }, BraveWallet.BlockchainToken>({ queryFn: async ( tokenArg, @@ -273,6 +197,7 @@ export const tokenEndpoints = ({ { type: 'UserBlockchainTokens', id: getAssetIdKey(tokenArg) } ] }), + updateUserAssetVisible: mutation({ queryFn: async ( { isVisible, token }, @@ -317,6 +242,7 @@ export const tokenEndpoints = ({ ] : ['UNKNOWN_ERROR'] }), + invalidateUserTokensRegistry: mutation({ queryFn: async (arg, { endpoint }, extraOptions, baseQuery) => { try { @@ -341,6 +267,7 @@ export const tokenEndpoints = ({ ] : ['UNKNOWN_ERROR'] }), + getTokenInfo: query< BraveWallet.BlockchainToken | null, Pick @@ -386,6 +313,87 @@ export const tokenEndpoints = ({ id: `${args.coin}-${args.chainId}-${args.contractAddress}` } ] + }), + + // Token Hiding + hideOrDeleteToken: mutation< + boolean, + { + mode: 'hide' | 'delete' + tokenId: string + } + >({ + queryFn: async (arg, { endpoint }, extraOptions, baseQuery) => { + try { + const { cache } = baseQuery(undefined) + + // only show the token in the "hidden" list + if (arg.mode === 'hide') { + const currentIds = getHiddenTokenIds() + if (currentIds.includes(arg.tokenId)) { + throw new Error('Token is already removed') + } + localStorage.setItem( + LOCAL_STORAGE_KEYS.USER_HIDDEN_TOKEN_IDS, + JSON.stringify(currentIds.concat(arg.tokenId)) + ) + } + + // prevent showing the token in any list if it is auto-discovered + if (arg.mode === 'delete') { + const currentIds = getDeletedTokenIds() + if (currentIds.includes(arg.tokenId)) { + throw new Error('Token is already deleted') + } + localStorage.setItem( + LOCAL_STORAGE_KEYS.USER_DELETED_TOKEN_IDS, + JSON.stringify(currentIds.concat(arg.tokenId)) + ) + } + + cache.clearUserTokensRegistry() + + return { + data: true + } + } catch (error) { + return handleEndpointError( + endpoint, + 'Unable to locally remove or delete token', + error + ) + } + }, + invalidatesTags: (res, err) => (err ? [] : ['UserBlockchainTokens']) + }), + + restoreHiddenToken: mutation< + boolean, + string // tokenId + >({ + queryFn: async (tokenId, { endpoint }, extraOptions, baseQuery) => { + try { + const { cache } = baseQuery(undefined) + + localStorage.setItem( + LOCAL_STORAGE_KEYS.USER_HIDDEN_TOKEN_IDS, + JSON.stringify(getHiddenTokenIds().filter((id) => id !== tokenId)) + ) + + cache.clearUserTokensRegistry() + + return { + data: true + } + } catch (error) { + return handleEndpointError( + endpoint, + 'Unable to locally remove or delete token', + error + ) + } + }, + invalidatesTags: ['UserBlockchainTokens'] }) } } diff --git a/components/brave_wallet_ui/common/slices/entities/blockchain-token.entity.ts b/components/brave_wallet_ui/common/slices/entities/blockchain-token.entity.ts index 8ba809949b39..f919b61b597e 100644 --- a/components/brave_wallet_ui/common/slices/entities/blockchain-token.entity.ts +++ b/components/brave_wallet_ui/common/slices/entities/blockchain-token.entity.ts @@ -36,6 +36,8 @@ export type BlockchainTokenEntityAdaptorState = ReturnType< idsByChainId: Record idsByCoinType: Record visibleTokenIds: string[] + hiddenTokenIds: string[] + deletedTokenIds: string[] visibleTokenIdsByChainId: Record visibleTokenIdsByCoinType: Record } @@ -46,6 +48,8 @@ BlockchainTokenEntityAdaptorState = { idsByChainId: {}, idsByCoinType: {}, visibleTokenIds: [], + hiddenTokenIds: [], + deletedTokenIds: [], visibleTokenIdsByChainId: {}, visibleTokenIdsByCoinType: {} } @@ -111,7 +115,9 @@ export const combineTokenRegistries = ( idsByChainId, visibleTokenIdsByChainId, idsByCoinType, - visibleTokenIdsByCoinType + visibleTokenIdsByCoinType, + deletedTokenIds: userTokensRegistry.deletedTokenIds, + hiddenTokenIds: userTokensRegistry.hiddenTokenIds }, getEntitiesListFromEntityState(tokensRegistry).concat( getEntitiesListFromEntityState(userTokensRegistry) @@ -268,3 +274,14 @@ export const selectAllVisibleUserAssetsFromQueryResult = createDraftSafeSelector([selectTokensRegistryFromQueryResult], (assets) => getEntitiesListFromEntityState(assets, assets.visibleTokenIds) ) + +/** + * Used to select only hidden NFTs from useGetUserTokensRegistryQuery + */ +export const selectHiddenNftsFromQueryResult = createDraftSafeSelector( + [selectTokensRegistryFromQueryResult], + (assets) => + getEntitiesListFromEntityState(assets, assets.hiddenTokenIds).filter( + (t) => t.isErc1155 || t.isErc721 || t.isNft + ) +) diff --git a/components/brave_wallet_ui/common/slices/wallet.slice.ts b/components/brave_wallet_ui/common/slices/wallet.slice.ts index 5cfc0ab7664d..327d9d211892 100644 --- a/components/brave_wallet_ui/common/slices/wallet.slice.ts +++ b/components/brave_wallet_ui/common/slices/wallet.slice.ts @@ -84,25 +84,9 @@ const defaultState: WalletState = { hidePortfolioBalances: window.localStorage.getItem(LOCAL_STORAGE_KEYS.HIDE_PORTFOLIO_BALANCES) === 'true', - removedFungibleTokenIds: JSON.parse( - localStorage.getItem(LOCAL_STORAGE_KEYS.USER_REMOVED_FUNGIBLE_TOKEN_IDS) || - '[]' - ), - removedNonFungibleTokenIds: JSON.parse( - localStorage.getItem( - LOCAL_STORAGE_KEYS.USER_REMOVED_NON_FUNGIBLE_TOKEN_IDS - ) || '[]' - ), - deletedNonFungibleTokenIds: JSON.parse( - localStorage.getItem( - LOCAL_STORAGE_KEYS.USER_DELETED_NON_FUNGIBLE_TOKEN_IDS - ) || '[]' - ), hidePortfolioNFTsTab: window.localStorage.getItem(LOCAL_STORAGE_KEYS.HIDE_PORTFOLIO_NFTS_TAB) === 'true', - removedNonFungibleTokens: [] as BraveWallet.BlockchainToken[], - deletedNonFungibleTokens: [] as BraveWallet.BlockchainToken[], filteredOutPortfolioNetworkKeys: parseJSONFromLocalStorage( 'FILTERED_OUT_PORTFOLIO_NETWORK_KEYS', makeInitialFilteredOutNetworkKeys() @@ -274,41 +258,6 @@ export const createWalletSlice = (initialState: WalletState = defaultState) => { state.hidePortfolioGraph = payload }, - setRemovedFungibleTokenIds( - state: WalletState, - { payload }: PayloadAction - ) { - state.removedFungibleTokenIds = payload - }, - - setRemovedNonFungibleTokenIds( - state: WalletState, - { payload }: PayloadAction - ) { - state.removedNonFungibleTokenIds = payload - }, - - setRemovedNonFungibleTokens( - state: WalletState, - { payload }: PayloadAction - ) { - state.removedNonFungibleTokens = payload - }, - - setDeletedNonFungibleTokenIds( - state: WalletState, - { payload }: PayloadAction - ) { - state.deletedNonFungibleTokenIds = payload - }, - - setDeletedNonFungibleTokens( - state: WalletState, - { payload }: PayloadAction - ) { - state.deletedNonFungibleTokens = payload - }, - setFilteredOutPortfolioNetworkKeys( state: WalletState, { payload }: PayloadAction diff --git a/components/brave_wallet_ui/components/desktop/views/nfts/components/nfts.tsx b/components/brave_wallet_ui/components/desktop/views/nfts/components/nfts.tsx index a28caf4f5820..9c82ff6a8a5f 100644 --- a/components/brave_wallet_ui/components/desktop/views/nfts/components/nfts.tsx +++ b/components/brave_wallet_ui/components/desktop/views/nfts/components/nfts.tsx @@ -15,8 +15,7 @@ import { useNftPin } from '../../../../../common/hooks/nft-pin' // selectors import { useSafeUISelector, - useSafeWalletSelector, - useUnsafeWalletSelector + useSafeWalletSelector } from '../../../../../common/hooks/use-safe-selector' import { UISelectors, WalletSelectors } from '../../../../../common/selectors' @@ -33,6 +32,7 @@ import { import { useGetNftDiscoveryEnabledStatusQuery, useGetSimpleHashSpamNftsQuery, + useGetUserTokensRegistryQuery, useSetNftDiscoveryEnabledMutation } from '../../../../../common/slices/api.slice' import { getBalance } from '../../../../../utils/balance-utils' @@ -43,6 +43,9 @@ import { import { useApiProxy } from '../../../../../common/hooks/use-api-proxy' import { getAssetIdKey } from '../../../../../utils/asset-utils' import { useQuery } from '../../../../../common/hooks/use-query' +import { + selectHiddenNftsFromQueryResult // +} from '../../../../../common/slices/entities/blockchain-token.entity' // components import SearchBar from '../../../../shared/search-bar' @@ -103,9 +106,6 @@ export const Nfts = (props: Props) => { const isNftPinningFeatureEnabled = useSafeWalletSelector( WalletSelectors.isNftPinningFeatureEnabled ) - const hiddenNfts = useUnsafeWalletSelector( - WalletSelectors.removedNonFungibleTokens - ) const selectedGroupAssetsByItem = useSafeWalletSelector( WalletSelectors.selectedGroupAssetsByItem ) @@ -116,9 +116,6 @@ export const Nfts = (props: Props) => { WalletSelectors.isRefreshingNetworksAndTokens ) const isPanel = useSafeUISelector(UISelectors.isPanel) - const deletedNonFungibleTokenIds = useUnsafeWalletSelector( - WalletSelectors.deletedNonFungibleTokenIds - ) // state const [searchValue, setSearchValue] = React.useState('') @@ -145,6 +142,15 @@ export const Nfts = (props: Props) => { const { data: isNftAutoDiscoveryEnabled } = useGetNftDiscoveryEnabledStatusQuery() const { data: simpleHashSpamNfts = [] } = useGetSimpleHashSpamNftsQuery() + const { hiddenNfts, deletedTokenIds } = useGetUserTokensRegistryQuery( + undefined, + { + selectFromResult: (result) => ({ + hiddenNfts: selectHiddenNftsFromQueryResult(result), + deletedTokenIds: result.data?.deletedTokenIds || [] + }) + } + ) // mutations const [setNftDiscovery] = useSetNftDiscoveryEnabledMutation() @@ -267,7 +273,7 @@ export const Nfts = (props: Props) => { // and deleted NFTs const excludedNftIds = userNonSpamNftIds .concat(hiddenNftsIds) - .concat(deletedNonFungibleTokenIds) + .concat(deletedTokenIds) const simpleHashList = simpleHashSpamNfts.filter( (nft) => !excludedNftIds.includes(getAssetIdKey(nft)) ) @@ -287,7 +293,7 @@ export const Nfts = (props: Props) => { simpleHashSpamNfts, hiddenNftsIds, userNonSpamNftIds, - deletedNonFungibleTokenIds + deletedTokenIds ]) const [sortedNfts, sortedHiddenNfts, sortedSpamNfts] = React.useMemo(() => { diff --git a/components/brave_wallet_ui/components/desktop/views/portfolio/components/nft-grid-view/nft-grid-view-item.tsx b/components/brave_wallet_ui/components/desktop/views/portfolio/components/nft-grid-view/nft-grid-view-item.tsx index 58df38d030d6..7c58a29ee4e0 100644 --- a/components/brave_wallet_ui/components/desktop/views/portfolio/components/nft-grid-view/nft-grid-view-item.tsx +++ b/components/brave_wallet_ui/components/desktop/views/portfolio/components/nft-grid-view/nft-grid-view-item.tsx @@ -16,8 +16,10 @@ import { } from '../../../../../../common/hooks/assets-management' import { useGetIpfsGatewayTranslatedNftUrlQuery, - useRemoveUserTokenMutation, // - useUpdateNftSpamStatusMutation + useRestoreHiddenTokenMutation, + useRemoveUserTokenMutation, + useUpdateNftSpamStatusMutation, + useHideOrDeleteTokenMutation } from '../../../../../../common/slices/api.slice' // actions @@ -30,6 +32,7 @@ import { WalletSelectors } from '../../../../../../common/selectors' // Utils import { stripERC20TokenImageURL } from '../../../../../../utils/string-utils' import { getLocale } from '../../../../../../../common/locale' +import { getAssetIdKey } from '../../../../../../utils/asset-utils' // components import { DecoratedNftIcon } from '../../../../../shared/nft-icon/decorated-nft-icon' @@ -80,12 +83,13 @@ export const NFTGridViewItem = (props: Props) => { // hooks const dispatch = useDispatch() - const { addOrRemoveTokenInLocalStorage, addNftToDeletedNftsList } = - useAssetManagement() + const { addNftToDeletedNftsList } = useAssetManagement() // mutations const [updateNftSpamStatus] = useUpdateNftSpamStatusMutation() const [removeUserToken] = useRemoveUserTokenMutation() + const [hideOrDeleteToken] = useHideOrDeleteTokenMutation() + const [restoreHiddenToken] = useRestoreHiddenTokenMutation() // methods const onToggleShowMore = React.useCallback( @@ -105,17 +109,20 @@ export const NFTGridViewItem = (props: Props) => { setShowMore(false) }, []) - const onHideNft = React.useCallback(() => { + const onHideNft = React.useCallback(async () => { setShowMore(false) - addOrRemoveTokenInLocalStorage(token, 'remove') + await hideOrDeleteToken({ + mode: 'hide', + tokenId: getAssetIdKey(token) + }) dispatch( WalletActions.refreshNetworksAndTokens({ skipBalancesRefresh: true }) ) - }, [token, addOrRemoveTokenInLocalStorage]) + }, [token, hideOrDeleteToken]) const onUnHideNft = React.useCallback(async () => { setShowMore(false) - addOrRemoveTokenInLocalStorage(token, 'add') + await restoreHiddenToken(getAssetIdKey(token)) if (isTokenSpam) { // remove from spam await updateNftSpamStatus({ token, status: false }) @@ -123,7 +130,7 @@ export const NFTGridViewItem = (props: Props) => { dispatch( WalletActions.refreshNetworksAndTokens({ skipBalancesRefresh: true }) ) - }, [token, addOrRemoveTokenInLocalStorage, isTokenSpam]) + }, [token, restoreHiddenToken, isTokenSpam]) const onUnSpam = async () => { setShowMore(false) diff --git a/components/brave_wallet_ui/components/desktop/views/portfolio/portfolio-nft-asset.tsx b/components/brave_wallet_ui/components/desktop/views/portfolio/portfolio-nft-asset.tsx index a6495a552871..bd6c27658f5d 100644 --- a/components/brave_wallet_ui/components/desktop/views/portfolio/portfolio-nft-asset.tsx +++ b/components/brave_wallet_ui/components/desktop/views/portfolio/portfolio-nft-asset.tsx @@ -15,16 +15,10 @@ import { getBalance } from '../../../../utils/balance-utils' import { makeSendRoute } from '../../../../utils/routes-utils' import Amount from '../../../../utils/amount' -// selectors -import { WalletSelectors } from '../../../../common/selectors' - // Components import { NftScreen } from '../../../../nft/components/nft-details/nft-screen' // Hooks -import { - useUnsafeWalletSelector // -} from '../../../../common/hooks/use-safe-selector' import { useGetNetworkQuery, useGetSimpleHashSpamNftsQuery, @@ -35,7 +29,8 @@ import { } from '../../../../common/hooks/use-scoped-balance-updater' import { useAccountsQuery } from '../../../../common/slices/api.slice.extra' import { - selectAllVisibleUserAssetsFromQueryResult // + selectAllVisibleUserAssetsFromQueryResult, // + selectHiddenNftsFromQueryResult } from '../../../../common/slices/entities/blockchain-token.entity' // Styled Components @@ -52,21 +47,17 @@ export const PortfolioNftAsset = () => { tokenId?: string }>() - // redux - const hiddenNfts = useUnsafeWalletSelector( - WalletSelectors.removedNonFungibleTokens - ) - // queries const { data: simpleHashNfts = [], isLoading: isLoadingSpamNfts } = useGetSimpleHashSpamNftsQuery() - const { userVisibleTokensInfo, isLoadingVisibleTokens } = + const { hiddenNfts, userVisibleTokensInfo, isLoadingTokens } = useGetUserTokensRegistryQuery(undefined, { selectFromResult: (result) => ({ + hiddenNfts: selectHiddenNftsFromQueryResult(result), userVisibleTokensInfo: selectAllVisibleUserAssetsFromQueryResult(result), - isLoadingVisibleTokens: result.isLoading + isLoadingTokens: result.isLoading }) }) @@ -153,7 +144,7 @@ export const PortfolioNftAsset = () => { if ( !selectedAssetFromParams && !isLoadingSpamNfts && - !isLoadingVisibleTokens && + !isLoadingTokens && userVisibleTokensInfo.length === 0 ) { return diff --git a/components/brave_wallet_ui/constants/types.ts b/components/brave_wallet_ui/constants/types.ts index 829a8d4229d2..52e56c3b8bb9 100644 --- a/components/brave_wallet_ui/constants/types.ts +++ b/components/brave_wallet_ui/constants/types.ts @@ -222,12 +222,7 @@ export interface WalletState { isAnkrBalancesFeatureEnabled: boolean hidePortfolioGraph: boolean hidePortfolioBalances: boolean - removedFungibleTokenIds: string[] - removedNonFungibleTokenIds: string[] - deletedNonFungibleTokenIds: string[] hidePortfolioNFTsTab: boolean - removedNonFungibleTokens: BraveWallet.BlockchainToken[] - deletedNonFungibleTokens: BraveWallet.BlockchainToken[] filteredOutPortfolioNetworkKeys: string[] filteredOutPortfolioAccountAddresses: string[] hidePortfolioSmallBalances: boolean diff --git a/components/brave_wallet_ui/stories/mock-data/mock-wallet-state.ts b/components/brave_wallet_ui/stories/mock-data/mock-wallet-state.ts index 55320b978839..e0414143660c 100644 --- a/components/brave_wallet_ui/stories/mock-data/mock-wallet-state.ts +++ b/components/brave_wallet_ui/stories/mock-data/mock-wallet-state.ts @@ -116,11 +116,6 @@ export const mockWalletState: WalletState = { isNftPinningFeatureEnabled: false, hidePortfolioBalances: false, hidePortfolioGraph: false, - removedFungibleTokenIds: [], - removedNonFungibleTokenIds: [], - removedNonFungibleTokens: [], - deletedNonFungibleTokenIds: [], - deletedNonFungibleTokens: [], hidePortfolioNFTsTab: false, filteredOutPortfolioNetworkKeys: [], filteredOutPortfolioAccountAddresses: [], diff --git a/components/brave_wallet_ui/utils/asset-utils.ts b/components/brave_wallet_ui/utils/asset-utils.ts index 930098c41368..a97e06786b9d 100644 --- a/components/brave_wallet_ui/utils/asset-utils.ts +++ b/components/brave_wallet_ui/utils/asset-utils.ts @@ -9,8 +9,8 @@ import { BraveWallet, SupportedTestNetworks } from '../constants/types' // utils import Amount from './amount' import { getRampNetworkPrefix } from './string-utils' - import { getNetworkLogo, makeNativeAssetLogo } from '../options/asset-options' +import { LOCAL_STORAGE_KEYS } from '../common/constants/local-storage-keys' export const getUniqueAssets = (assets: BraveWallet.BlockchainToken[]) => { return assets.filter((asset, index) => { @@ -306,3 +306,26 @@ export const checkIfTokenNeedsNetworkIcon = ( */ export const isStripeSupported = () => navigator.language.toLowerCase() === 'en-us' + +export const getHiddenTokenIds = (): string[] => { + return JSON.parse( + localStorage.getItem(LOCAL_STORAGE_KEYS.USER_HIDDEN_TOKEN_IDS) || '[]' + ) +} + +export const getDeletedTokenIds = (): string[] => { + return JSON.parse( + localStorage.getItem(LOCAL_STORAGE_KEYS.USER_DELETED_TOKEN_IDS) || '[]' + ) +} + +export const getHiddenOrDeletedTokenIdsList = () => { + return [...getDeletedTokenIds(), ...getHiddenTokenIds()] +} + +export const isTokenRemovedInLocalStorage = ( + tokenId: string, + removedIds: string[] +) => { + return removedIds.includes(tokenId) +}