Skip to content

Commit

Permalink
feat(wallet): allow hiding unowned NFTs
Browse files Browse the repository at this point in the history
  • Loading branch information
josheleonard committed May 23, 2024
1 parent c14f1cf commit ff57232
Show file tree
Hide file tree
Showing 12 changed files with 451 additions and 252 deletions.
1 change: 1 addition & 0 deletions components/brave_wallet/browser/brave_wallet_constants.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ inline constexpr char kSimpleHashBraveProxyUrl[] =
"https://simplehash.wallet.brave.com";

inline constexpr webui::LocalizedString kLocalizedStrings[] = {
{"braveWalletHideNotOwnedNfTs", IDS_BRAVE_WALLET_HIDE_NOT_OWNED_NF_TS},
{"braveWalletNoRoutesFound", IDS_BRAVE_WALLET_NO_ROUTES_FOUND},
{"braveWalletPrivateKeyImportType",
IDS_BRAVE_WALLET_PRIVATE_KEY_IMPORT_TYPE},
Expand Down
41 changes: 41 additions & 0 deletions components/brave_wallet_ui/common/async/base-query-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ export class BaseQueryCache {
private _nftMetadataRegistry: Record<string, NFTMetadataReturnType> = {}
public rewardsInfo: BraveRewardsInfo | undefined = undefined
public balanceScannerSupportedChains: string[] | undefined = undefined
public spamNftsForAccountRegistry: Record<
string, // accountUniqueId
BraveWallet.BlockchainToken[]
> = {}

getWalletInfo = async () => {
if (!this.walletInfo) {
Expand Down Expand Up @@ -483,6 +487,43 @@ export class BaseQueryCache {
return this._nftMetadataRegistry[tokenId]
}

getSpamNftsForAccountId = async (accountId: BraveWallet.AccountId) => {
if (!this.spamNftsForAccountRegistry[accountId.uniqueKey]) {
const { braveWalletService } = getAPIProxy()
const { address, coin } = accountId
const networksRegistry = await cache.getNetworksRegistry()

const chainIds = networksRegistry.ids.map(
(network) => networksRegistry.entities[network]!.chainId
)

let currentCursor: string | null = null
const accountSpamNfts = []

do {
const {
tokens,
cursor
}: {
tokens: BraveWallet.BlockchainToken[]
cursor: string | null
} = await braveWalletService.getSimpleHashSpamNFTs(
address,
chainIds,
coin,
currentCursor
)

accountSpamNfts.push(...tokens)
currentCursor = cursor
} while (currentCursor)

this.spamNftsForAccountRegistry[accountId.uniqueKey] = accountSpamNfts
}

return this.spamNftsForAccountRegistry[accountId.uniqueKey]
}

// Brave Rewards
getBraveRewardsInfo = async () => {
if (!this.rewardsInfo) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@ export const LOCAL_STORAGE_KEYS = {
CURRENT_PANEL: 'BRAVE_WALLET_CURRENT_PANEL',
LAST_VISITED_PANEL: 'BRAVE_WALLET_LAST_VISITED_PANEL',
TOKEN_BALANCES: 'BRAVE_WALLET_TOKEN_BALANCES2',
SPAM_TOKEN_BALANCES: 'SPAM_TOKEN_BALANCES',
SAVED_SESSION_ROUTE: 'BRAVE_WALLET_SAVED_SESSION_ROUTE',
USER_HIDDEN_TOKEN_IDS: 'BRAVE_WALLET_USER_HIDDEN_TOKEN_IDS',
USER_DELETED_TOKEN_IDS: 'BRAVE_WALLET_USER_DELETED_TOKEN_IDS'
USER_DELETED_TOKEN_IDS: 'BRAVE_WALLET_USER_DELETED_TOKEN_IDS',
HIDE_UNOWNED_NFTS: 'HIDE_UNOWNED_NFTS'
} as const

const LOCAL_STORAGE_KEYS_DEPRECATED = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
GetTokenBalancesRegistryArg //
} from '../slices/endpoints/token_balances.endpoints'

type Arg = Pick<GetTokenBalancesRegistryArg, 'networks'> & {
type Arg = Pick<GetTokenBalancesRegistryArg, 'networks' | 'isSpamRegistry'> & {
accounts: BraveWallet.AccountInfo[]
}

Expand Down Expand Up @@ -45,7 +45,8 @@ export const useBalancesFetcher = (arg: Arg | typeof skipToken) => {
supportedKeyrings
})
),
useAnkrBalancesFeature
useAnkrBalancesFeature,
isSpamRegistry: arg.isSpamRegistry
}
: skipToken,
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,46 +206,25 @@ export const nftsEndpoints = ({
}
}
}),
getSimpleHashSpamNfts: query<BraveWallet.BlockchainToken[], void>({
queryFn: async (_arg, { endpoint }, _extraOptions, baseQuery) => {
try {
const { data: api, cache } = baseQuery(undefined)
const { braveWalletService } = api

const networksRegistry = await cache.getNetworksRegistry()
/** will get spam for all accounts if accounts arg is not provided */
getSimpleHashSpamNfts: query<
BraveWallet.BlockchainToken[],
void | undefined | { accounts: BraveWallet.AccountInfo[] }
>({
queryFn: async (arg, { endpoint }, _extraOptions, baseQuery) => {
try {
const { cache } = baseQuery(undefined)

const chainIds = networksRegistry.ids.map(
(network) => networksRegistry.entities[network]!.chainId
)
const lookupAccounts =
arg?.accounts ?? (await cache.getAllAccounts()).accounts

const { accounts } = await cache.getAllAccounts()
const spamNfts = (
await mapLimit(
accounts,
lookupAccounts,
10,
async (account: BraveWallet.AccountInfo) => {
let currentCursor: string | null = null
const accountSpamNfts = []

do {
const {
tokens,
cursor
}: {
tokens: BraveWallet.BlockchainToken[]
cursor: string | null
} = await braveWalletService.getSimpleHashSpamNFTs(
account.address,
chainIds,
account.accountId.coin,
currentCursor
)

accountSpamNfts.push(...tokens)
currentCursor = cursor
} while (currentCursor)

return accountSpamNfts
return await cache.getSpamNftsForAccountId(account.accountId)
}
)
).flat(1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,15 @@ import {
baseQueryFunction
} from '../../async/base-query-cache'
import {
getPersistedPortfolioSpamTokenBalances,
getPersistedPortfolioTokenBalances,
setPersistedPortfolioSpamTokenBalances,
setPersistedPortfolioTokenBalances
} from '../../../utils/local-storage-utils'
import { getIsRewardsNetwork } from '../../../utils/rewards_utils'
import {
blockchainTokenEntityAdaptorInitialState //
} from '../entities/blockchain-token.entity'

type BalanceNetwork = Pick<
BraveWallet.NetworkInfo,
Expand Down Expand Up @@ -92,6 +97,9 @@ export type GetTokenBalancesRegistryArg = {
accountIds: BraveWallet.AccountId[]
networks: BalanceNetwork[]
useAnkrBalancesFeature: boolean
/** if true, only spam NFT balances will be fetched, if falsey, only user
* token balances will be fetched */
isSpamRegistry?: boolean
}

function mergeTokenBalancesRegistry(
Expand Down Expand Up @@ -222,8 +230,10 @@ export const tokenBalancesEndpoints = ({
TokenBalancesRegistry | null,
GetTokenBalancesRegistryArg
>({
queryFn: function () {
const persistedBalances = getPersistedPortfolioTokenBalances()
queryFn: function (arg) {
const persistedBalances = arg.isSpamRegistry
? getPersistedPortfolioSpamTokenBalances()
: getPersistedPortfolioTokenBalances()

// return null so we can tell if we have data or not to start with
return {
Expand Down Expand Up @@ -357,7 +367,13 @@ export const tokenBalancesEndpoints = ({
networkSupportsAccount(network, accountId)
)

const userTokens = await cache.getUserTokensRegistry()
const userTokensRegistry = arg.isSpamRegistry
? blockchainTokenEntityAdaptorInitialState
: await cache.getUserTokensRegistry()

const spamTokens = arg.isSpamRegistry
? await cache.getSpamNftsForAccountId(accountId)
: []

if (nonAnkrSupportedAccountNetworks.length) {
await eachLimit(
Expand All @@ -366,6 +382,22 @@ export const tokenBalancesEndpoints = ({
async (network: BraveWallet.NetworkInfo) => {
assert(coinTypesMapping[network.coin] !== undefined)
try {
const tokens = arg.isSpamRegistry
? spamTokens.filter(
(token) =>
token.coin === network.coin &&
token.chainId === network.chainId
)
: getEntitiesListFromEntityState(
userTokensRegistry,
userTokensRegistry.idsByChainId[
networkEntityAdapter.selectId({
coin: network.coin,
chainId: network.chainId
})
]
)

await fetchTokenBalanceRegistryForAccountsAndChainIds({
args:
network.coin === CoinTypes.SOL
Expand All @@ -381,15 +413,7 @@ export const tokenBalancesEndpoints = ({
accountId,
coin: coinTypesMapping[network.coin],
chainId: network.chainId,
tokens: getEntitiesListFromEntityState(
userTokens,
userTokens.idsByChainId[
networkEntityAdapter.selectId({
coin: network.coin,
chainId: network.chainId
})
]
)
tokens: tokens
}
],
cache,
Expand All @@ -416,13 +440,19 @@ export const tokenBalancesEndpoints = ({
return tokenBalancesRegistry
})

const persistedBalances = getPersistedPortfolioTokenBalances()
setPersistedPortfolioTokenBalances(
mergeTokenBalancesRegistry(
persistedBalances,
tokenBalancesRegistry
)
const persistedBalances = arg.isSpamRegistry
? getPersistedPortfolioSpamTokenBalances()
: getPersistedPortfolioTokenBalances()

const mergedRegistry = mergeTokenBalancesRegistry(
persistedBalances,
tokenBalancesRegistry
)
if (arg.isSpamRegistry) {
setPersistedPortfolioSpamTokenBalances(mergedRegistry)
} else {
setPersistedPortfolioTokenBalances(mergedRegistry)
}
} catch (error) {
handleEndpointError(
'getTokenBalancesRegistry.onCacheEntryAdded',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ export const PortfolioFiltersModal = (props: Props) => {
LOCAL_STORAGE_KEYS.SHOW_NETWORK_LOGO_ON_NFTS,
false
)
const [hideUnownedNfts, setHideUnownedNfts] = useSyncedLocalStorage<boolean>(
LOCAL_STORAGE_KEYS.HIDE_UNOWNED_NFTS,
false
)

// queries
const { data: defaultFiatCurrency = 'usd' } = useGetDefaultFiatCurrencyQuery()
Expand All @@ -128,6 +132,8 @@ export const PortfolioFiltersModal = (props: Props) => {
const [showNetworkLogo, setShowNetworkLogo] = React.useState(
showNetworkLogoOnNfts
)
const [hideUnownedNftsToggle, setHideUnownedNftsToggle] =
React.useState(hideUnownedNfts)

// Memos
const hideSmallBalancesDescription = React.useMemo(() => {
Expand All @@ -151,6 +157,7 @@ export const PortfolioFiltersModal = (props: Props) => {
setSelectedAssetFilter(selectedAssetFilterOption)
setHidePortfolioSmallBalances(hideSmallBalances)
setShowNetworkLogoOnNfts(showNetworkLogo)
setHideUnownedNfts(hideUnownedNftsToggle)
onClose()
}, [
setFilteredOutPortfolioNetworkKeys,
Expand All @@ -165,6 +172,8 @@ export const PortfolioFiltersModal = (props: Props) => {
hideSmallBalances,
setShowNetworkLogoOnNfts,
showNetworkLogo,
setHideUnownedNfts,
hideUnownedNftsToggle,
onClose
])

Expand Down Expand Up @@ -197,6 +206,14 @@ export const PortfolioFiltersModal = (props: Props) => {
setIsSelected={() => setShowNetworkLogo((prev) => !prev)}
/>

<FilterToggleSection
title={getLocale('braveWalletHideNotOwnedNfTs')}
description={''}
icon='web3'
isSelected={hideUnownedNftsToggle}
setIsSelected={() => setHideUnownedNftsToggle((prev) => !prev)}
/>

{/* Disabled until Spam NFTs feature is implemented in core */}
{/* <FilterToggleSection
title={getLocale('braveWalletShowSpamNftsTitle')}
Expand Down

0 comments on commit ff57232

Please sign in to comment.