diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Nfts/BreadCrumb/useBreadCrumbModel.tsx b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Nfts/BreadCrumb/useBreadCrumbModel.tsx index 00be2b1f1c1c..ea7470522ba3 100644 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Nfts/BreadCrumb/useBreadCrumbModel.tsx +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Nfts/BreadCrumb/useBreadCrumbModel.tsx @@ -2,39 +2,27 @@ import { useSelector } from "react-redux"; import { useHistory, useParams } from "react-router-dom"; import { State } from "~/renderer/reducers"; import { accountSelector } from "~/renderer/reducers/accounts"; -import { nftsByCollections } from "@ledgerhq/live-nft"; import { useCallback, useMemo } from "react"; import { ProtoNFT } from "@ledgerhq/types-live"; import { DropDownItemType } from "~/renderer/components/DropDownSelector"; import { setTrackingSource } from "~/renderer/analytics/TrackPage"; -import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; -import { isThresholdValid, useNftGalleryFilter } from "@ledgerhq/live-nft-react"; +import { useNftCollections } from "~/renderer/hooks/nfts/useNftCollections"; const useBreadCrumbModel = () => { const history = useHistory(); const { id, collectionAddress } = useParams<{ id?: string; collectionAddress?: string }>(); - const nftsFromSimplehashFeature = useFeature("nftsFromSimplehash"); - const thresold = nftsFromSimplehashFeature?.params?.threshold; const account = useSelector((state: State) => - id ? accountSelector(state, { accountId: id }) : null, + id ? accountSelector(state, { accountId: id }) : undefined, ); - const { nfts } = useNftGalleryFilter({ - nftsOwned: account?.nfts || [], - addresses: String(account?.freshAddress), - chains: [String(account?.currency.id)], - threshold: isThresholdValid(thresold) ? Number(thresold) : 75, + const { collections } = useNftCollections({ + account, }); - const collections = useMemo( - () => nftsByCollections(nftsFromSimplehashFeature?.enabled ? nfts : account?.nfts), - [account?.nfts, nfts, nftsFromSimplehashFeature], - ); - const items: DropDownItemType[] = useMemo( () => - Object.entries(collections).map(([contract, nfts]: [string, ProtoNFT[]]) => ({ + collections.map(([contract, nfts]: [string, ProtoNFT[]]) => ({ key: contract, label: contract, content: nfts[0], diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Nfts/Collections/useNftCollectionsModel.tsx b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Nfts/Collections/useNftCollectionsModel.tsx index d14d62a8e132..ae215c1c4490 100644 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Nfts/Collections/useNftCollectionsModel.tsx +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Nfts/Collections/useNftCollectionsModel.tsx @@ -1,16 +1,10 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { Account, ProtoNFT } from "@ledgerhq/types-live"; -import { useDispatch, useSelector } from "react-redux"; +import { useDispatch } from "react-redux"; import { useHistory } from "react-router-dom"; import { openModal } from "~/renderer/actions/modals"; -import { hiddenNftCollectionsSelector } from "~/renderer/reducers/settings"; -import { nftsByCollections } from "@ledgerhq/live-nft/index"; -import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; -import { isThresholdValid, useNftGalleryFilter } from "@ledgerhq/live-nft-react"; -import { - filterHiddenCollections, - mapCollectionsToStructure, -} from "LLD/features/Collectibles/utils/collectionUtils"; +import { mapCollectionsToStructure } from "LLD/features/Collectibles/utils/collectionUtils"; +import { useNftCollections } from "~/renderer/hooks/nfts/useNftCollections"; type NftsInTheCollections = { contract: string; @@ -27,9 +21,6 @@ const INCREMENT = 5; export const useNftCollectionsModel = ({ account }: Props) => { const history = useHistory(); const dispatch = useDispatch(); - const nftsFromSimplehashFeature = useFeature("nftsFromSimplehash"); - const thresold = nftsFromSimplehashFeature?.params?.threshold; - const hiddenNftCollections = useSelector(hiddenNftCollectionsSelector); const [numberOfVisibleCollections, setNumberOfVisibleCollections] = useState(INCREMENT); const [displayShowMore, setDisplayShowMore] = useState(false); @@ -53,20 +44,10 @@ export const useNftCollectionsModel = ({ account }: Props) => { history.push(`/account/${account.id}/nft-collection`); }, [account.id, history]); - const { nfts, fetchNextPage, hasNextPage } = useNftGalleryFilter({ - nftsOwned: account.nfts || [], - addresses: account.freshAddress, - chains: [account.currency.id], - threshold: isThresholdValid(thresold) ? Number(thresold) : 75, + const { fetchNextPage, hasNextPage, collections, collectionsLength } = useNftCollections({ + account, }); - const collections = useMemo( - () => nftsByCollections(nftsFromSimplehashFeature?.enabled ? nfts : account.nfts), - [account.nfts, nfts, nftsFromSimplehashFeature], - ); - - const collectionsLength = Object.keys(collections).length; - const onShowMore = useCallback(() => { setNumberOfVisibleCollections(numberOfVisibleCollections => Math.min(numberOfVisibleCollections + INCREMENT, collectionsLength), @@ -74,21 +55,15 @@ export const useNftCollectionsModel = ({ account }: Props) => { if (hasNextPage) fetchNextPage(); }, [collectionsLength, fetchNextPage, hasNextPage]); - const filteredCollections = useMemo( - () => filterHiddenCollections(collections, hiddenNftCollections, account.id), - [account.id, collections, hiddenNftCollections], - ); - const nftsInTheCollection: NftsInTheCollections[] = useMemo( - () => - mapCollectionsToStructure(filteredCollections, numberOfVisibleCollections, onOpenCollection), - [filteredCollections, numberOfVisibleCollections, onOpenCollection], + () => mapCollectionsToStructure(collections, numberOfVisibleCollections, onOpenCollection), + [collections, numberOfVisibleCollections, onOpenCollection], ); useEffect(() => { - const moreToShow = numberOfVisibleCollections < filteredCollections.length; + const moreToShow = numberOfVisibleCollections < collections.length; setDisplayShowMore(moreToShow); - }, [numberOfVisibleCollections, filteredCollections.length]); + }, [numberOfVisibleCollections, collections.length]); return { nftsInTheCollection, diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Nfts/screens/Gallery/useNftGalleryModel.tsx b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Nfts/screens/Gallery/useNftGalleryModel.tsx index 00f3910aac3a..d58c3b864797 100644 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Nfts/screens/Gallery/useNftGalleryModel.tsx +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Nfts/screens/Gallery/useNftGalleryModel.tsx @@ -2,14 +2,10 @@ import { useHistory, useParams } from "react-router-dom"; import { useDispatch, useSelector } from "react-redux"; import { State } from "~/renderer/reducers"; import { accountSelector } from "~/renderer/reducers/accounts"; -import { hiddenNftCollectionsSelector } from "~/renderer/reducers/settings"; -import { useNftGalleryFilter, isThresholdValid } from "@ledgerhq/live-nft-react"; -import { nftsByCollections } from "@ledgerhq/live-nft"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { openModal } from "~/renderer/actions/modals"; import { useOnScreen } from "LLD/hooks/useOnScreen"; -import useFeature from "@ledgerhq/live-common/featureFlags/useFeature"; -import { BlockchainEVM } from "@ledgerhq/live-nft/supported"; +import { useNftCollections } from "~/renderer/hooks/nfts/useNftCollections"; const defaultNumberOfVisibleNfts = 10; @@ -18,31 +14,17 @@ const useNftGalleryModel = () => { const history = useHistory(); const { id } = useParams<{ id: string }>(); - const nftsFromSimplehashFeature = useFeature("nftsFromSimplehash"); - const threshold = nftsFromSimplehashFeature?.params?.threshold; - const listFooterRef = useRef(null); const [maxVisibleNFTs, setMaxVisibleNFTs] = useState(defaultNumberOfVisibleNfts); - const { account, hiddenNftCollections } = useSelector((state: State) => ({ + const { account } = useSelector((state: State) => ({ account: accountSelector(state, { accountId: id }), - hiddenNftCollections: hiddenNftCollectionsSelector(state), })); - const { nfts, fetchNextPage, hasNextPage } = useNftGalleryFilter({ - nftsOwned: account?.nfts || [], - addresses: account?.freshAddress || "", - chains: [account?.currency.id ?? BlockchainEVM.Ethereum], - threshold: isThresholdValid(threshold) ? Number(threshold) : 75, + const { fetchNextPage, hasNextPage, collections, allNfts } = useNftCollections({ + account, }); - const collections = useMemo(() => { - const allNfts = nftsFromSimplehashFeature?.enabled ? nfts : account?.nfts; - return Object.entries(nftsByCollections(allNfts)).filter( - ([contract]) => !hiddenNftCollections.includes(`${account?.id}|${contract}`), - ); - }, [account?.id, account?.nfts, hiddenNftCollections, nfts, nftsFromSimplehashFeature?.enabled]); - useEffect(() => { if (collections.length < 1) { history.push(`/account/${account?.id}/`); @@ -70,13 +52,13 @@ const useNftGalleryModel = () => { }, [hasNextPage, fetchNextPage]); useOnScreen({ - enabled: maxVisibleNFTs < nfts?.length, + enabled: maxVisibleNFTs < allNfts?.length, onIntersect: updateMaxVisibleNtfs, target: listFooterRef, threshold: 0.5, }); - const nftsByCollection = nfts.reduce( + const nftsByCollection = allNfts.reduce( (acc, nft) => { const collectionKey = nft.contract || "-"; if (!acc[collectionKey]) { @@ -85,12 +67,11 @@ const useNftGalleryModel = () => { acc[collectionKey].push(nft); return acc; }, - {} as Record, + {} as Record, ); return { account, - hiddenNftCollections, nftsByCollection, listFooterRef, collections, diff --git a/apps/ledger-live-desktop/src/renderer/Default.tsx b/apps/ledger-live-desktop/src/renderer/Default.tsx index 332883c6189b..421e5089469b 100644 --- a/apps/ledger-live-desktop/src/renderer/Default.tsx +++ b/apps/ledger-live-desktop/src/renderer/Default.tsx @@ -56,7 +56,7 @@ import { isLocked as isLockedSelector } from "~/renderer/reducers/application"; import { useAutoDismissPostOnboardingEntryPoint } from "@ledgerhq/live-common/postOnboarding/hooks/index"; import { setShareAnalytics, setSharePersonalizedRecommendations } from "./actions/settings"; import useEnv from "@ledgerhq/live-common/hooks/useEnv"; -import { useSyncNFTsWithAccounts } from "./hooks/useSyncNFTsWithAccounts"; +import { useSyncNFTsWithAccounts } from "./hooks/nfts/useSyncNFTsWithAccounts"; const PlatformCatalog = lazy(() => import("~/renderer/screens/platform")); const Dashboard = lazy(() => import("~/renderer/screens/dashboard")); diff --git a/apps/ledger-live-desktop/src/renderer/actions/settings.ts b/apps/ledger-live-desktop/src/renderer/actions/settings.ts index e90520966f32..bc199fe40776 100644 --- a/apps/ledger-live-desktop/src/renderer/actions/settings.ts +++ b/apps/ledger-live-desktop/src/renderer/actions/settings.ts @@ -218,6 +218,12 @@ export const hideNftCollection = (collectionId: string) => ({ type: "HIDE_NFT_COLLECTION", payload: collectionId, }); + +export const whitelistNftCollection = (collectionId: string) => ({ + type: "WHITELIST_NFT_COLLECTION", + payload: collectionId, +}); + export const hideOrdinalsAsset = (inscriptionId: string) => ({ type: "HIDE_ORDINALS_ASSET", payload: inscriptionId, @@ -252,6 +258,10 @@ export const unhideNftCollection = (collectionId: string) => ({ type: "UNHIDE_NFT_COLLECTION", payload: collectionId, }); +export const unwhitelistNftCollection = (collectionId: string) => ({ + type: "UNWHITELIST_NFT_COLLECTION", + payload: collectionId, +}); export const unhideOrdinalsAsset = (inscriptionId: string) => ({ type: "UNHIDE_ORDINALS_ASSET", payload: inscriptionId, diff --git a/apps/ledger-live-desktop/src/renderer/components/Breadcrumb/NFTCrumb.tsx b/apps/ledger-live-desktop/src/renderer/components/Breadcrumb/NFTCrumb.tsx index 58d168582774..f26dd379142f 100644 --- a/apps/ledger-live-desktop/src/renderer/components/Breadcrumb/NFTCrumb.tsx +++ b/apps/ledger-live-desktop/src/renderer/components/Breadcrumb/NFTCrumb.tsx @@ -1,7 +1,6 @@ import React, { useCallback, useMemo, memo } from "react"; import { useHistory, useParams } from "react-router-dom"; import { useSelector } from "react-redux"; -import { nftsByCollections } from "@ledgerhq/live-nft"; import { accountSelector } from "~/renderer/reducers/accounts"; import DropDownSelector, { DropDownItemType } from "~/renderer/components/DropDownSelector"; import Button from "~/renderer/components/Button"; @@ -14,8 +13,7 @@ import { setTrackingSource } from "~/renderer/analytics/TrackPage"; import CollectionName from "~/renderer/components/Nft/CollectionName"; import { ProtoNFT } from "@ledgerhq/types-live"; import { State } from "~/renderer/reducers"; -import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; -import { isThresholdValid, useNftGalleryFilter } from "@ledgerhq/live-nft-react"; +import { useNftCollections } from "~/renderer/hooks/nfts/useNftCollections"; const LabelWithMeta = ({ item, @@ -38,38 +36,29 @@ const LabelWithMeta = ({ const NFTCrumb = () => { const history = useHistory(); - const nftsFromSimplehashFeature = useFeature("nftsFromSimplehash"); - const thresold = nftsFromSimplehashFeature?.params?.threshold; const { id, collectionAddress } = useParams<{ id?: string; collectionAddress?: string }>(); const account = useSelector((state: State) => id ? accountSelector(state, { accountId: id, }) - : null, + : undefined, ); - const { nfts } = useNftGalleryFilter({ - nftsOwned: account?.nfts || [], - addresses: String(account?.freshAddress), - chains: [String(account?.currency.id)], - threshold: isThresholdValid(thresold) ? Number(thresold) : 75, + const { collections } = useNftCollections({ + account, }); - const collections = useMemo( - () => nftsByCollections(nftsFromSimplehashFeature?.enabled ? nfts : account?.nfts), - [account?.nfts, nfts, nftsFromSimplehashFeature], - ); - const items: DropDownItemType[] = useMemo( () => - Object.entries(collections).map(([contract, nfts]: [string, ProtoNFT[]]) => ({ + collections.map(([contract, nfts]: [string, ProtoNFT[]]) => ({ key: contract, label: contract, content: nfts[0], })), [collections], ); + const activeItem: DropDownItemType | undefined | null = useMemo( () => items.find(item => item.key === collectionAddress) || items[0], [collectionAddress, items], diff --git a/apps/ledger-live-desktop/src/renderer/components/ContextMenu/NFTContextMenu.tsx b/apps/ledger-live-desktop/src/renderer/components/ContextMenu/NFTContextMenu.tsx index bbd3e824f31a..70f1af44ee04 100644 --- a/apps/ledger-live-desktop/src/renderer/components/ContextMenu/NFTContextMenu.tsx +++ b/apps/ledger-live-desktop/src/renderer/components/ContextMenu/NFTContextMenu.tsx @@ -1,7 +1,7 @@ import React, { memo } from "react"; import ContextMenuItem from "./ContextMenuItem"; import { Account, ProtoNFT, NFTMetadata } from "@ledgerhq/types-live"; -import useNftLinks from "~/renderer/hooks/useNftLinks"; +import useNftLinks from "~/renderer/hooks/nfts/useNftLinks"; type Props = { account: Account; diff --git a/apps/ledger-live-desktop/src/renderer/components/CustomImage/NFTGallerySelector.tsx b/apps/ledger-live-desktop/src/renderer/components/CustomImage/NFTGallerySelector.tsx index 309e5d84c53d..39b28538b99b 100644 --- a/apps/ledger-live-desktop/src/renderer/components/CustomImage/NFTGallerySelector.tsx +++ b/apps/ledger-live-desktop/src/renderer/components/CustomImage/NFTGallerySelector.tsx @@ -2,16 +2,15 @@ import React, { useMemo, useRef, useState } from "react"; import { useSelector } from "react-redux"; import { Flex, Grid, InfiniteLoader, Text } from "@ledgerhq/react-ui"; import { NFTMetadata } from "@ledgerhq/types-live"; -import { accountsSelector, orderedVisibleNftsSelector } from "../../reducers/accounts"; +import { accountsSelector, orderedVisibleNftsSelector } from "~/renderer/reducers/accounts"; import NftGalleryEmptyState from "./NftGalleryEmptyState"; import isEqual from "lodash/isEqual"; import NFTItem from "./NFTItem"; import { useTranslation } from "react-i18next"; import styled from "styled-components"; import { useOnScreen } from "~/renderer/screens/nft/useOnScreen"; -import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; import { getEnv } from "@ledgerhq/live-env"; -import { isThresholdValid, useNftGalleryFilter } from "@ledgerhq/live-nft-react"; +import { useNftCollections } from "~/renderer/hooks/nfts/useNftCollections"; const ScrollContainer = styled(Flex).attrs({ flexDirection: "column", @@ -32,8 +31,6 @@ type Props = { const NFTGallerySelector = ({ handlePickNft, selectedNftId }: Props) => { const SUPPORTED_NFT_CURRENCIES = getEnv("NFT_CURRENCIES"); - const nftsFromSimplehashFeature = useFeature("nftsFromSimplehash"); - const threshold = nftsFromSimplehashFeature?.params?.threshold; const accounts = useSelector(accountsSelector); const nftsOrdered = useSelector(orderedVisibleNftsSelector, isEqual); @@ -47,26 +44,19 @@ const NFTGallerySelector = ({ handlePickNft, selectedNftId }: Props) => { [accounts], ); - const { - nfts: nftsFiltered, - fetchNextPage, - hasNextPage, - } = useNftGalleryFilter({ - nftsOwned: nftsOrdered || [], + const { fetchNextPage, hasNextPage, allNfts } = useNftCollections({ + nftsOwned: nftsOrdered, addresses: addresses, chains: SUPPORTED_NFT_CURRENCIES, - threshold: isThresholdValid(threshold) ? Number(threshold) : 75, }); - const nfts = nftsFromSimplehashFeature?.enabled ? nftsFiltered : nftsOrdered; - const { t } = useTranslation(); const [displayedCount, setDisplayedCount] = useState(10); const content = useMemo( () => - nfts.slice(0, displayedCount).map((nft, index) => { + allNfts.slice(0, displayedCount).map((nft, index) => { const { id } = nft; return ( { /> ); }), - [nfts, displayedCount, selectedNftId, handlePickNft], + [allNfts, displayedCount, selectedNftId, handlePickNft], ); const loaderContainerRef = useRef(null); @@ -91,13 +81,13 @@ const NFTGallerySelector = ({ handlePickNft, selectedNftId }: Props) => { }; useOnScreen({ - enabled: displayedCount < nfts.length, + enabled: displayedCount < allNfts.length, onIntersect: updateDisplayable, target: loaderContainerRef, threshold: 0.5, }); - if (nfts.length <= 0) return ; + if (allNfts.length <= 0) return ; return ( @@ -108,7 +98,7 @@ const NFTGallerySelector = ({ handlePickNft, selectedNftId }: Props) => { {content} - {displayedCount < nfts.length ? ( + {displayedCount < allNfts.length ? ( diff --git a/apps/ledger-live-desktop/src/renderer/components/OperationsList/Operation.tsx b/apps/ledger-live-desktop/src/renderer/components/OperationsList/Operation.tsx index 8b962a07f3df..79cf5176108d 100644 --- a/apps/ledger-live-desktop/src/renderer/components/OperationsList/Operation.tsx +++ b/apps/ledger-live-desktop/src/renderer/components/OperationsList/Operation.tsx @@ -15,7 +15,7 @@ import { confirmationsNbForCurrencySelector } from "~/renderer/reducers/settings import { isConfirmedOperation } from "@ledgerhq/live-common/operation"; import { useAccountUnit } from "~/renderer/hooks/useAccountUnit"; import { State } from "~/renderer/reducers"; -import { useAccountName } from "../../reducers/wallet"; +import { useAccountName } from "~/renderer/reducers/wallet"; const OperationRow = styled(Box).attrs(() => ({ horizontal: true, diff --git a/apps/ledger-live-desktop/src/renderer/components/Web3AppWebview/PlatformAPIWebview.tsx b/apps/ledger-live-desktop/src/renderer/components/Web3AppWebview/PlatformAPIWebview.tsx index 42edf84b62ae..d6139d5825f1 100644 --- a/apps/ledger-live-desktop/src/renderer/components/Web3AppWebview/PlatformAPIWebview.tsx +++ b/apps/ledger-live-desktop/src/renderer/components/Web3AppWebview/PlatformAPIWebview.tsx @@ -25,7 +25,7 @@ import { } from "@ledgerhq/live-common/platform/react"; import trackingWrapper from "@ledgerhq/live-common/platform/tracking"; import { openModal } from "../../actions/modals"; -import { flattenAccountsSelector } from "../../reducers/accounts"; +import { flattenAccountsSelector } from "~/renderer/reducers/accounts"; import BigSpinner from "../BigSpinner"; import { track } from "~/renderer/analytics/segment"; import { diff --git a/apps/ledger-live-desktop/src/renderer/components/Web3AppWebview/WalletAPIWebview.tsx b/apps/ledger-live-desktop/src/renderer/components/Web3AppWebview/WalletAPIWebview.tsx index c1237d6124bb..a2a5016f063d 100644 --- a/apps/ledger-live-desktop/src/renderer/components/Web3AppWebview/WalletAPIWebview.tsx +++ b/apps/ledger-live-desktop/src/renderer/components/Web3AppWebview/WalletAPIWebview.tsx @@ -35,9 +35,9 @@ import { setDrawer } from "~/renderer/drawers/Provider"; import { shareAnalyticsSelector } from "~/renderer/reducers/settings"; import { walletSelector } from "~/renderer/reducers/wallet"; import { getStoreValue, setStoreValue } from "~/renderer/store"; -import { updateAccountWithUpdater } from "../../actions/accounts"; -import { openModal } from "../../actions/modals"; -import { flattenAccountsSelector } from "../../reducers/accounts"; +import { updateAccountWithUpdater } from "~/renderer/actions/accounts"; +import { openModal } from "~/renderer/actions/modals"; +import { flattenAccountsSelector } from "~/renderer/reducers/accounts"; import BigSpinner from "../BigSpinner"; import { NetworkErrorScreen } from "./NetworkError"; import { NoAccountOverlay } from "./NoAccountOverlay"; diff --git a/apps/ledger-live-desktop/src/renderer/drawers/NFTViewerDrawer/ExternalViewerButton.tsx b/apps/ledger-live-desktop/src/renderer/drawers/NFTViewerDrawer/ExternalViewerButton.tsx index 365f1b619c92..d2ba072556b2 100644 --- a/apps/ledger-live-desktop/src/renderer/drawers/NFTViewerDrawer/ExternalViewerButton.tsx +++ b/apps/ledger-live-desktop/src/renderer/drawers/NFTViewerDrawer/ExternalViewerButton.tsx @@ -5,7 +5,7 @@ import Box from "~/renderer/components/Box"; import Button from "~/renderer/components/Button"; import DropDownSelector, { DropDownItem } from "~/renderer/components/DropDownSelector"; import IconExternal from "~/renderer/icons/ExternalLink"; -import useNftLinks from "~/renderer/hooks/useNftLinks"; +import useNftLinks from "~/renderer/hooks/nfts/useNftLinks"; import { setDrawer } from "~/renderer/drawers/Provider"; import { Account, ProtoNFT, NFTMetadata } from "@ledgerhq/types-live"; import { Icons } from "@ledgerhq/react-ui"; diff --git a/apps/ledger-live-desktop/src/renderer/families/bitcoin/SendRecipientFields.tsx b/apps/ledger-live-desktop/src/renderer/families/bitcoin/SendRecipientFields.tsx index d856fdfe9827..2f41c9d6ffaf 100644 --- a/apps/ledger-live-desktop/src/renderer/families/bitcoin/SendRecipientFields.tsx +++ b/apps/ledger-live-desktop/src/renderer/families/bitcoin/SendRecipientFields.tsx @@ -7,7 +7,7 @@ import { connect } from "react-redux"; import Alert from "~/renderer/components/Alert"; import TranslatedError from "~/renderer/components/TranslatedError"; import { State } from "~/renderer/reducers"; -import { confirmationsNbForCurrencySelector } from "../../reducers/settings"; +import { confirmationsNbForCurrencySelector } from "~/renderer/reducers/settings"; // FIXME: ConfirmationNB seems to be specific. // So we can't do diff --git a/apps/ledger-live-desktop/src/renderer/hooks/nfts/__tests__/useHideSpamCollection.test.ts b/apps/ledger-live-desktop/src/renderer/hooks/nfts/__tests__/useHideSpamCollection.test.ts new file mode 100644 index 000000000000..f6406bd162c4 --- /dev/null +++ b/apps/ledger-live-desktop/src/renderer/hooks/nfts/__tests__/useHideSpamCollection.test.ts @@ -0,0 +1,48 @@ +import { hideNftCollection } from "~/renderer/actions/settings"; +import { useHideSpamCollection } from "../useHideSpamCollection"; +import { renderHook } from "tests/testUtils"; +import { INITIAL_STATE } from "~/renderer/reducers/settings"; +import { useDispatch } from "react-redux"; + +jest.mock("react-redux", () => ({ + ...jest.requireActual("react-redux"), + useDispatch: jest.fn(), +})); + +const mockDispatch = jest.fn(); + +describe("useHideSpamCollection", () => { + beforeEach(() => { + (useDispatch as jest.Mock).mockReturnValue(mockDispatch); + mockDispatch.mockClear(); + }); + + it("should dispatch hideNftCollection action if collection is not whitelisted", () => { + const { result } = renderHook(() => useHideSpamCollection(), { + initialState: { + settings: { + ...INITIAL_STATE, + whitelistedNftCollections: ["collectionA", "collectionB"], + hiddenNftCollections: [], + }, + }, + }); + result.current.hideSpamCollection("collectionC"); + + expect(mockDispatch).toHaveBeenCalledWith(hideNftCollection("collectionC")); + }); + + it("should not dispatch hideNftCollection action if collection is whitelisted", () => { + const { result } = renderHook(() => useHideSpamCollection(), { + initialState: { + settings: { + hiddenNftCollections: [], + whitelistedNftCollections: ["collectionA", "collectionB"], + }, + }, + }); + result.current.hideSpamCollection("collectionA"); + + expect(mockDispatch).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/ledger-live-desktop/src/renderer/hooks/nfts/__tests__/useSyncNFTsWitHAccount.test.ts b/apps/ledger-live-desktop/src/renderer/hooks/nfts/__tests__/useSyncNFTsWitHAccount.test.ts new file mode 100644 index 000000000000..d43721130372 --- /dev/null +++ b/apps/ledger-live-desktop/src/renderer/hooks/nfts/__tests__/useSyncNFTsWitHAccount.test.ts @@ -0,0 +1,90 @@ +import { useSelector } from "react-redux"; +import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; +import { useCheckNftAccount } from "@ledgerhq/live-nft-react"; +import { useHideSpamCollection } from "../useHideSpamCollection"; +import { useSyncNFTsWithAccounts } from "../useSyncNFTsWithAccounts"; + +import { accountsSelector, orderedVisibleNftsSelector } from "~/renderer/reducers/accounts"; +import { renderHook } from "@testing-library/react"; + +jest.mock("react-redux", () => ({ + useSelector: jest.fn(), +})); + +jest.mock("@ledgerhq/live-common/featureFlags/index", () => ({ + useFeature: jest.fn(), +})); + +jest.mock("../useHideSpamCollection", () => ({ + useHideSpamCollection: jest.fn(), +})); + +jest.mock("@ledgerhq/live-nft-react", () => ({ + useCheckNftAccount: jest.fn(), + isThresholdValid: jest.fn(), + getThreshold: jest.fn(), +})); + +describe("useSyncNFTsWithAccounts", () => { + const mockUseSelector = useSelector as jest.Mock; + const mockUseFeature = useFeature as jest.Mock; + const mockUseHideSpamCollection = useHideSpamCollection as jest.Mock; + const mockUseCheckNftAccount = useCheckNftAccount as jest.Mock; + + beforeEach(() => { + jest.clearAllMocks(); + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it("should refetch periodically based on TIMER", () => { + const mockRefetch = jest.fn(); + const mockAccounts = [{ freshAddress: "0x123" }, { freshAddress: "0x456" }]; + + mockUseFeature.mockReturnValue({ enabled: true }); + mockUseHideSpamCollection.mockReturnValue({ enabled: true, hideSpamCollection: jest.fn() }); + mockUseSelector.mockImplementation(selector => { + if (selector === accountsSelector) return mockAccounts; + if (selector === orderedVisibleNftsSelector) return []; + return []; + }); + mockUseCheckNftAccount.mockReturnValue({ refetch: mockRefetch }); + + renderHook(() => useSyncNFTsWithAccounts()); + + jest.advanceTimersByTime(5 * 60 * 60 * 1000); + + expect(mockRefetch).toHaveBeenCalledTimes(1); + }); + + it("should refetch immediately when a new account is added", () => { + const mockRefetch = jest.fn(); + const initialAccounts = [{ freshAddress: "0x123" }]; + const updatedAccounts = [...initialAccounts, { freshAddress: "0x789" }]; + + mockUseFeature.mockReturnValue({ enabled: true }); + mockUseHideSpamCollection.mockReturnValue({ enabled: true, hideSpamCollection: jest.fn() }); + mockUseSelector + .mockImplementationOnce(selector => { + if (selector === accountsSelector) return initialAccounts; + if (selector === orderedVisibleNftsSelector) return []; + return []; + }) + .mockImplementationOnce(selector => { + if (selector === accountsSelector) return updatedAccounts; + if (selector === orderedVisibleNftsSelector) return []; + return []; + }); + + mockUseCheckNftAccount.mockReturnValue({ refetch: mockRefetch }); + + const { rerender } = renderHook(() => useSyncNFTsWithAccounts()); + + rerender(); + + expect(mockRefetch).toHaveBeenCalledTimes(2); // 1 for initial render & 1 for adding new account + }); +}); diff --git a/apps/ledger-live-desktop/src/renderer/hooks/useHideSpamCollection.ts b/apps/ledger-live-desktop/src/renderer/hooks/nfts/useHideSpamCollection.ts similarity index 68% rename from apps/ledger-live-desktop/src/renderer/hooks/useHideSpamCollection.ts rename to apps/ledger-live-desktop/src/renderer/hooks/nfts/useHideSpamCollection.ts index e4ac73c9cc1a..7772d51dddee 100644 --- a/apps/ledger-live-desktop/src/renderer/hooks/useHideSpamCollection.ts +++ b/apps/ledger-live-desktop/src/renderer/hooks/nfts/useHideSpamCollection.ts @@ -1,20 +1,21 @@ import { useCallback } from "react"; import { useDispatch, useSelector } from "react-redux"; import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; +import { whitelistedNftCollectionsSelector } from "~/renderer/reducers/settings"; import { hideNftCollection } from "~/renderer/actions/settings"; -import { hiddenNftCollectionsSelector } from "../reducers/settings"; export function useHideSpamCollection() { const spamFilteringTxFeature = useFeature("spamFilteringTx"); - const hiddenNftCollections = useSelector(hiddenNftCollectionsSelector); + const whitelistedNftCollections = useSelector(whitelistedNftCollectionsSelector); + const dispatch = useDispatch(); const hideSpamCollection = useCallback( (collection: string) => { - if (!hiddenNftCollections.includes(collection)) { + if (!whitelistedNftCollections.includes(collection)) { dispatch(hideNftCollection(collection)); } }, - [dispatch, hiddenNftCollections], + [dispatch, whitelistedNftCollections], ); return { diff --git a/apps/ledger-live-desktop/src/renderer/hooks/nfts/useNftCollections.ts b/apps/ledger-live-desktop/src/renderer/hooks/nfts/useNftCollections.ts new file mode 100644 index 000000000000..16832100f6e5 --- /dev/null +++ b/apps/ledger-live-desktop/src/renderer/hooks/nfts/useNftCollections.ts @@ -0,0 +1,75 @@ +import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; +import { decodeCollectionId, getThreshold, useNftGalleryFilter } from "@ledgerhq/live-nft-react"; +import { nftsByCollections } from "@ledgerhq/live-nft/index"; +import { BlockchainEVM } from "@ledgerhq/live-nft/supported"; +import { Account, ProtoNFT } from "@ledgerhq/types-live"; +import { useMemo } from "react"; +import { useSelector } from "react-redux"; +import { + hiddenNftCollectionsSelector, + whitelistedNftCollectionsSelector, +} from "~/renderer/reducers/settings"; + +export function useNftCollections({ + account, + nftsOwned, + addresses, + chains, +}: { + account?: Account; + nftsOwned?: ProtoNFT[]; + addresses?: string; + chains?: string[]; +}) { + const nftsFromSimplehashFeature = useFeature("nftsFromSimplehash"); + const threshold = nftsFromSimplehashFeature?.params?.threshold; + const simplehashEnabled = nftsFromSimplehashFeature?.enabled; + + const whitelistNft = useSelector(whitelistedNftCollectionsSelector); + const hiddenNftCollections = useSelector(hiddenNftCollectionsSelector); + + const nftsOwnedToCheck = useMemo(() => account?.nfts ?? nftsOwned, [account?.nfts, nftsOwned]); + + const whitelistedNfts = useMemo( + () => + nftsOwnedToCheck?.filter(nft => + whitelistNft + .map(collection => decodeCollectionId(collection).contractAddress) + .includes(nft.contract), + ) ?? [], + [nftsOwnedToCheck, whitelistNft], + ); + + const { nfts, fetchNextPage, hasNextPage } = useNftGalleryFilter({ + nftsOwned: account?.nfts ?? nftsOwned ?? [], + addresses: account?.freshAddress ?? addresses ?? "", + chains: account + ? [account.currency.id || BlockchainEVM.Ethereum] + : chains ?? [BlockchainEVM.Ethereum], + threshold: getThreshold(threshold), + }); + + const allNfts = useMemo( + () => (simplehashEnabled ? [...nfts, ...whitelistedNfts] : account?.nfts || nftsOwned || []), + [simplehashEnabled, nfts, whitelistedNfts, account, nftsOwned], + ); + + const collections = useMemo( + () => + Object.entries(nftsByCollections(allNfts)).filter( + ([contract]) => !hiddenNftCollections.includes(`${account?.id}|${contract}`), + ), + [account?.id, allNfts, hiddenNftCollections], + ); + + const collectionsLength = Object.keys(collections).length; + + return { + collections, + collectionsLength, + fetchNextPage, + hasNextPage, + nfts, + allNfts, + }; +} diff --git a/apps/ledger-live-desktop/src/renderer/hooks/useNftLinks.ts b/apps/ledger-live-desktop/src/renderer/hooks/nfts/useNftLinks.ts similarity index 96% rename from apps/ledger-live-desktop/src/renderer/hooks/useNftLinks.ts rename to apps/ledger-live-desktop/src/renderer/hooks/nfts/useNftLinks.ts index f6dc75342e95..f3ef4a4977e0 100644 --- a/apps/ledger-live-desktop/src/renderer/hooks/useNftLinks.ts +++ b/apps/ledger-live-desktop/src/renderer/hooks/nfts/useNftLinks.ts @@ -10,11 +10,11 @@ import IconOpensea from "~/renderer/icons/Opensea"; import IconRarible from "~/renderer/icons/Rarible"; import { openURL } from "~/renderer/linking"; import { getMetadataMediaTypes } from "~/helpers/nft"; -import { setDrawer } from "../drawers/Provider"; +import { setDrawer } from "../../drawers/Provider"; import CustomImage from "~/renderer/screens/customImage"; import NFTViewerDrawer from "~/renderer/drawers/NFTViewerDrawer"; -import { ContextMenuItemType } from "../components/ContextMenu/ContextMenuWrapper"; -import { devicesModelListSelector } from "../reducers/settings"; +import { ContextMenuItemType } from "../../components/ContextMenu/ContextMenuWrapper"; +import { devicesModelListSelector } from "~/renderer/reducers/settings"; function safeList(items: (ContextMenuItemType | "" | undefined)[]): ContextMenuItemType[] { return items.filter(Boolean) as ContextMenuItemType[]; diff --git a/apps/ledger-live-desktop/src/renderer/hooks/useSyncNFTsWithAccounts.ts b/apps/ledger-live-desktop/src/renderer/hooks/nfts/useSyncNFTsWithAccounts.ts similarity index 74% rename from apps/ledger-live-desktop/src/renderer/hooks/useSyncNFTsWithAccounts.ts rename to apps/ledger-live-desktop/src/renderer/hooks/nfts/useSyncNFTsWithAccounts.ts index 622b05852077..b795bb62fc0c 100644 --- a/apps/ledger-live-desktop/src/renderer/hooks/useSyncNFTsWithAccounts.ts +++ b/apps/ledger-live-desktop/src/renderer/hooks/nfts/useSyncNFTsWithAccounts.ts @@ -1,9 +1,9 @@ import { useEffect, useMemo, useState } from "react"; import { useHideSpamCollection } from "./useHideSpamCollection"; -import { isThresholdValid, useCheckNftAccount } from "@ledgerhq/live-nft-react"; +import { getThreshold, useCheckNftAccount } from "@ledgerhq/live-nft-react"; import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; import { useSelector } from "react-redux"; -import { accountsSelector, orderedVisibleNftsSelector } from "../reducers/accounts"; +import { accountsSelector, orderedVisibleNftsSelector } from "~/renderer/reducers/accounts"; import isEqual from "lodash/isEqual"; import { getEnv } from "@ledgerhq/live-env"; @@ -38,9 +38,7 @@ const TIMER = 5 * 60 * 60 * 1000; // 5 hours = 18000000 ms export function useSyncNFTsWithAccounts() { const SUPPORTED_NFT_CURRENCIES = getEnv("NFT_CURRENCIES"); const nftsFromSimplehashFeature = useFeature("nftsFromSimplehash"); - const threshold = isThresholdValid(Number(nftsFromSimplehashFeature?.params?.threshold)) - ? Number(nftsFromSimplehashFeature?.params?.threshold) - : 75; + const threshold = getThreshold(nftsFromSimplehashFeature?.params?.threshold); const { enabled, hideSpamCollection } = useHideSpamCollection(); @@ -62,29 +60,42 @@ export function useSyncNFTsWithAccounts() { }, []); }, [accounts]); - const [groupToFetch, setGroupToFetch] = useState(addressGroups[0]); + const [groupToFetch, setGroupToFetch] = useState( + addressGroups.length > 0 ? addressGroups[0] : [], + ); const [, setCurrentIndex] = useState(0); + const { refetch } = useCheckNftAccount({ + addresses: groupToFetch.join(","), + nftsOwned, + chains: SUPPORTED_NFT_CURRENCIES, + threshold, + action: hideSpamCollection, + enabled, + }); + + // Refetch with new last group when addressGroups length changes + useEffect(() => { + if (enabled) { + const newIndex = addressGroups.length - 1; + setCurrentIndex(newIndex); + setGroupToFetch(addressGroups[newIndex] || []); + refetch(); + } + }, [addressGroups.length, addressGroups, refetch, enabled]); + + // Regular interval-based rotation through groups useEffect(() => { if (!enabled) return; + const interval = setInterval(() => { setCurrentIndex(prevIndex => { const nextIndex = (prevIndex + 1) % addressGroups.length; setGroupToFetch(addressGroups[nextIndex]); - return nextIndex; }); }, TIMER); return () => clearInterval(interval); }, [addressGroups, enabled]); - - useCheckNftAccount({ - addresses: groupToFetch.join(","), - nftsOwned, - chains: SUPPORTED_NFT_CURRENCIES, - threshold, - action: hideSpamCollection, - enabled, - }); } diff --git a/apps/ledger-live-desktop/src/renderer/modals/HideNftCollection/Footer.tsx b/apps/ledger-live-desktop/src/renderer/modals/HideNftCollection/Footer.tsx index 5d07f94bdf86..da297a4fed5f 100644 --- a/apps/ledger-live-desktop/src/renderer/modals/HideNftCollection/Footer.tsx +++ b/apps/ledger-live-desktop/src/renderer/modals/HideNftCollection/Footer.tsx @@ -2,15 +2,22 @@ import React, { useCallback } from "react"; import Button from "~/renderer/components/Button"; import { Trans } from "react-i18next"; import Box from "~/renderer/components/Box"; -import { useDispatch } from "react-redux"; -import { hideNftCollection } from "~/renderer/actions/settings"; +import { useDispatch, useSelector } from "react-redux"; +import { hideNftCollection, unwhitelistNftCollection } from "~/renderer/actions/settings"; +import { whitelistedNftCollectionsSelector } from "~/renderer/reducers/settings"; const Footer = ({ onClose, collectionId }: { onClose: () => void; collectionId: string }) => { const dispatch = useDispatch(); + const whitelistedNftCollections = useSelector(whitelistedNftCollectionsSelector); + const confirmHideNftCollection = useCallback( (collectionId: string) => { + if (whitelistedNftCollections.includes(collectionId)) { + dispatch(unwhitelistNftCollection(collectionId)); + } + dispatch(hideNftCollection(collectionId)); }, - [dispatch], + [dispatch, whitelistedNftCollections], ); return ( diff --git a/apps/ledger-live-desktop/src/renderer/reducers/settings.ts b/apps/ledger-live-desktop/src/renderer/reducers/settings.ts index 38cabc8fb56e..deb7e81dfdbe 100644 --- a/apps/ledger-live-desktop/src/renderer/reducers/settings.ts +++ b/apps/ledger-live-desktop/src/renderer/reducers/settings.ts @@ -82,6 +82,7 @@ export type SettingsState = { starredAccountIds?: string[]; blacklistedTokenIds: string[]; hiddenNftCollections: string[]; + whitelistedNftCollections: string[]; hiddenOrdinalsAsset: string[]; deepLinkUrl: string | undefined | null; lastSeenCustomImage: { @@ -182,6 +183,7 @@ export const INITIAL_STATE: SettingsState = { latestFirmware: null, blacklistedTokenIds: [], hiddenNftCollections: [], + whitelistedNftCollections: [], hiddenOrdinalsAsset: [], deepLinkUrl: null, firstTimeLend: false, @@ -228,6 +230,8 @@ type HandlersPayloads = { BLACKLIST_TOKEN: string; UNHIDE_NFT_COLLECTION: string; HIDE_NFT_COLLECTION: string; + WHITELIST_NFT_COLLECTION: string; + UNWHITELIST_NFT_COLLECTION: string; UNHIDE_ORDINALS_ASSET: string; HIDE_ORDINALS_ASSET: string; LAST_SEEN_DEVICE_INFO: { @@ -334,9 +338,25 @@ const handlers: SettingsHandlers = { const collections = state.hiddenNftCollections; return { ...state, - hiddenNftCollections: [...collections, collectionId], + hiddenNftCollections: [...new Set(collections.concat(collectionId))], }; }, + + UNWHITELIST_NFT_COLLECTION: (state, { payload: collectionId }) => { + const ids = state.whitelistedNftCollections; + return { + ...state, + whitelistedNftCollections: ids.filter(id => id !== collectionId), + }; + }, + WHITELIST_NFT_COLLECTION: (state, { payload: collectionId }) => { + const collections = state.whitelistedNftCollections; + return { + ...state, + whitelistedNftCollections: [...new Set(collections.concat(collectionId))], + }; + }, + UNHIDE_ORDINALS_ASSET: (state, { payload: inscriptionId }) => { const ids = state.hiddenOrdinalsAsset; return { @@ -761,6 +781,8 @@ export const enableLearnPageStagingUrlSelector = (state: State) => state.settings.enableLearnPageStagingUrl; export const blacklistedTokenIdsSelector = (state: State) => state.settings.blacklistedTokenIds; export const hiddenNftCollectionsSelector = (state: State) => state.settings.hiddenNftCollections; +export const whitelistedNftCollectionsSelector = (state: State) => + state.settings.whitelistedNftCollections; export const hiddenOrdinalsAssetSelector = (state: State) => state.settings.hiddenOrdinalsAsset; export const hasCompletedOnboardingSelector = (state: State) => state.settings.hasCompletedOnboarding || getEnv("SKIP_ONBOARDING"); diff --git a/apps/ledger-live-desktop/src/renderer/screens/nft/Collections/Collections.tsx b/apps/ledger-live-desktop/src/renderer/screens/nft/Collections/Collections.tsx index 345a47071e1d..f28a12c3c182 100644 --- a/apps/ledger-live-desktop/src/renderer/screens/nft/Collections/Collections.tsx +++ b/apps/ledger-live-desktop/src/renderer/screens/nft/Collections/Collections.tsx @@ -1,14 +1,12 @@ import React, { useState, useCallback, useEffect, useMemo, memo } from "react"; -import { nftsByCollections } from "@ledgerhq/live-nft"; import { Account, NFT, ProtoNFT } from "@ledgerhq/types-live"; -import { useDispatch, useSelector } from "react-redux"; +import { useDispatch } from "react-redux"; import { useTranslation } from "react-i18next"; import { useHistory } from "react-router-dom"; import styled from "styled-components"; import { TokenShowMoreIndicator, IconAngleDown } from "~/renderer/screens/account/TokensList"; import TableContainer, { TableHeader } from "~/renderer/components/TableContainer"; import LabelWithExternalIcon from "~/renderer/components/LabelWithExternalIcon"; -import { hiddenNftCollectionsSelector } from "~/renderer/reducers/settings"; import { supportLinkByTokenType } from "~/config/urls"; import { openModal } from "~/renderer/actions/modals"; import { track } from "~/renderer/analytics/segment"; @@ -19,9 +17,7 @@ import Text from "~/renderer/components/Text"; import { openURL } from "~/renderer/linking"; import Box from "~/renderer/components/Box"; import Row from "./Row"; -import { isThresholdValid, useCheckNftAccount } from "@ledgerhq/live-nft-react"; -import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; -import { useHideSpamCollection } from "~/renderer/hooks/useHideSpamCollection"; +import { useNftCollections } from "~/renderer/hooks/nfts/useNftCollections"; const INCREMENT = 5; const EmptyState = styled.div` @@ -46,13 +42,15 @@ type Props = { account: Account; }; const Collections = ({ account }: Props) => { - const nftsFromSimplehashFeature = useFeature("nftsFromSimplehash"); - const thresold = nftsFromSimplehashFeature?.params?.threshold; - const dispatch = useDispatch(); const { t } = useTranslation(); const history = useHistory(); const [numberOfVisibleCollections, setNumberOfVisibleCollections] = useState(INCREMENT); + + const { fetchNextPage, hasNextPage, collectionsLength, collections } = useNftCollections({ + account, + }); + const onOpenGallery = useCallback(() => { history.push(`/account/${account.id}/nft-collection`); }, [account.id, history]); @@ -72,21 +70,6 @@ const Collections = ({ account }: Props) => { [account.id, history], ); - const { enabled, hideSpamCollection } = useHideSpamCollection(); - - const { nfts, fetchNextPage, hasNextPage } = useCheckNftAccount({ - nftsOwned: account.nfts || [], - addresses: account.freshAddress, - chains: [account.currency.id], - threshold: isThresholdValid(thresold) ? Number(thresold) : 75, - ...(enabled && { action: hideSpamCollection }), - }); - - const collections = useMemo( - () => nftsByCollections(nftsFromSimplehashFeature?.enabled ? nfts : account.nfts), - [account.nfts, nfts, nftsFromSimplehashFeature], - ); - const collectionsLength = Object.keys(collections).length; const onShowMore = useCallback(() => { setNumberOfVisibleCollections(numberOfVisibleCollections => Math.min(numberOfVisibleCollections + INCREMENT, collectionsLength), @@ -95,17 +78,10 @@ const Collections = ({ account }: Props) => { fetchNextPage(); } }, [collectionsLength, fetchNextPage, hasNextPage]); - const hiddenNftCollections = useSelector(hiddenNftCollectionsSelector); - const filteredCollections = useMemo( - () => - Object.entries(collections).filter( - ([contract]) => !hiddenNftCollections.includes(`${account.id}|${contract}`), - ), - [account.id, collections, hiddenNftCollections], - ); + const visibleCollections = useMemo( () => - filteredCollections + collections .slice(0, numberOfVisibleCollections) .map(([contract, nfts]: [string, (ProtoNFT | NFT)[]]) => ( { nfts={nfts} /> )), - [account, filteredCollections, numberOfVisibleCollections, onOpenCollection], + [account, collections, numberOfVisibleCollections, onOpenCollection], ); useEffect(() => { @@ -169,7 +145,7 @@ const Collections = ({ account }: Props) => { )} - {filteredCollections.length > numberOfVisibleCollections ? ( + {collections.length > numberOfVisibleCollections ? ( diff --git a/apps/ledger-live-desktop/src/renderer/screens/nft/Gallery/Gallery.tsx b/apps/ledger-live-desktop/src/renderer/screens/nft/Gallery/Gallery.tsx index 67a85d695cdc..3a2d838c84ed 100644 --- a/apps/ledger-live-desktop/src/renderer/screens/nft/Gallery/Gallery.tsx +++ b/apps/ledger-live-desktop/src/renderer/screens/nft/Gallery/Gallery.tsx @@ -4,8 +4,6 @@ import { useSelector, useDispatch } from "react-redux"; import { useTranslation } from "react-i18next"; import { accountSelector } from "~/renderer/reducers/accounts"; import { openModal } from "~/renderer/actions/modals"; -import { nftsByCollections } from "@ledgerhq/live-nft"; -import { hiddenNftCollectionsSelector } from "~/renderer/reducers/settings"; import styled from "styled-components"; import IconSend from "~/renderer/icons/Send"; import CollectionName from "~/renderer/components/Nft/CollectionName"; @@ -20,9 +18,7 @@ import { State } from "~/renderer/reducers"; import { ProtoNFT } from "@ledgerhq/types-live"; import theme from "@ledgerhq/react-ui/styles/theme"; import { useOnScreen } from "../useOnScreen"; -import { isThresholdValid, useNftGalleryFilter } from "@ledgerhq/live-nft-react"; -import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; -import { BlockchainEVM } from "@ledgerhq/live-nft/supported"; +import { useNftCollections } from "~/renderer/hooks/nfts/useNftCollections"; const SpinnerContainer = styled.div` display: flex; @@ -53,9 +49,7 @@ const Footer = styled.footer` `; const Gallery = () => { - const nftsFromSimplehashFeature = useFeature("nftsFromSimplehash"); - const thresold = nftsFromSimplehashFeature?.params?.threshold; - + const history = useHistory(); const { t } = useTranslation(); const dispatch = useDispatch(); const { id } = useParams<{ id: string }>(); @@ -64,24 +58,11 @@ const Gallery = () => { accountId: id, }), ); - const history = useHistory(); - const hiddenNftCollections = useSelector(hiddenNftCollectionsSelector); - const { nfts, fetchNextPage, hasNextPage } = useNftGalleryFilter({ - nftsOwned: account?.nfts || [], - addresses: account?.freshAddress || "", - chains: [account?.currency.id ?? BlockchainEVM.Ethereum], - threshold: isThresholdValid(thresold) ? Number(thresold) : 75, + const { fetchNextPage, hasNextPage, collections } = useNftCollections({ + account, }); - const collections = useMemo( - () => - Object.entries( - nftsByCollections(nftsFromSimplehashFeature?.enabled ? nfts : account?.nfts), - ).filter(([contract]) => !hiddenNftCollections.includes(`${account?.id}|${contract}`)), - [account?.id, account?.nfts, hiddenNftCollections, nfts, nftsFromSimplehashFeature?.enabled], - ); - // Should redirect to the account page if there is not NFT anymore in the page. useEffect(() => { if (collections.length <= 0) { diff --git a/apps/ledger-live-desktop/src/renderer/screens/settings/index.tsx b/apps/ledger-live-desktop/src/renderer/screens/settings/index.tsx index 4c91b144986b..7ca6a725fb62 100644 --- a/apps/ledger-live-desktop/src/renderer/screens/settings/index.tsx +++ b/apps/ledger-live-desktop/src/renderer/screens/settings/index.tsx @@ -13,7 +13,7 @@ import SectionAccounts from "./sections/Accounts"; import SectionAbout from "./sections/About"; import SectionHelp from "./sections/Help"; import { setTrackingSource } from "~/renderer/analytics/TrackPage"; -import { developerModeSelector } from "../../reducers/settings"; +import { developerModeSelector } from "~/renderer/reducers/settings"; const getItems = (t: (a: string) => string, devMode?: boolean) => { const items = [ diff --git a/apps/ledger-live-desktop/src/renderer/screens/settings/sections/About/index.tsx b/apps/ledger-live-desktop/src/renderer/screens/settings/sections/About/index.tsx index 914561ff0f32..590424444728 100644 --- a/apps/ledger-live-desktop/src/renderer/screens/settings/sections/About/index.tsx +++ b/apps/ledger-live-desktop/src/renderer/screens/settings/sections/About/index.tsx @@ -5,11 +5,11 @@ import { getEnv } from "@ledgerhq/live-env"; import { SettingsSectionBody as Body, SettingsSectionRow as Row } from "../../SettingsSection"; import RowItem from "../../RowItem"; import ReleaseNotesButton from "./ReleaseNotesButton"; -import { setDeveloperMode } from "../../../../actions/settings"; +import { setDeveloperMode } from "~/renderer/actions/settings"; import { useDispatch, useSelector } from "react-redux"; import { useToasts } from "@ledgerhq/live-common/notifications/ToastProvider/index"; import { v4 as uuidv4 } from "uuid"; -import { developerModeSelector } from "../../../../reducers/settings"; +import { developerModeSelector } from "~/renderer/reducers/settings"; import { useLocalizedUrl } from "~/renderer/hooks/useLocalizedUrls"; import { urls } from "~/config/urls"; diff --git a/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Accounts/HiddenNFTCollections/index.tsx b/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Accounts/HiddenNFTCollections/index.tsx index e046da17d952..85bb0002c96b 100644 --- a/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Accounts/HiddenNFTCollections/index.tsx +++ b/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Accounts/HiddenNFTCollections/index.tsx @@ -2,7 +2,7 @@ import React, { useState, useCallback } from "react"; import { useDispatch, useSelector } from "react-redux"; import { useTranslation } from "react-i18next"; import styled from "styled-components"; -import { unhideNftCollection } from "~/renderer/actions/settings"; +import { unhideNftCollection, whitelistNftCollection } from "~/renderer/actions/settings"; import { hiddenNftCollectionsSelector } from "~/renderer/reducers/settings"; import { SettingsSection as Section, SettingsSectionRow as Row } from "../../../SettingsSection"; @@ -11,6 +11,25 @@ import Track from "~/renderer/analytics/Track"; import IconAngleDown from "~/renderer/icons/AngleDown"; import { HiddenNftCollectionRow } from "./row"; +import { decodeCollectionId } from "@ledgerhq/live-nft-react"; + +// Styled components and layout + +const Body = styled(Box)` + &:not(:empty) { + padding: 0 20px; + } +`; + +const Show = styled(Box).attrs<{ visible?: boolean }>({})<{ visible?: boolean }>` + transform: rotate(${p => (p.visible ? 0 : 270)}deg); +`; + +const Collections = styled(Box)` + &:hover { + cursor: pointer; + } +`; export default function HiddenNftCollections() { const { t } = useTranslation(); @@ -22,6 +41,7 @@ export default function HiddenNftCollections() { const onUnhideCollection = useCallback( (collectionId: string) => { dispatch(unhideNftCollection(collectionId)); + dispatch(whitelistNftCollection(collectionId)); }, [dispatch], ); @@ -56,7 +76,7 @@ export default function HiddenNftCollections() { {sectionVisible && ( {hiddenNftCollections.map(collectionId => { - const [accountId, contractAddress] = collectionId.split("|"); + const { accountId, contractAddress } = decodeCollectionId(collectionId); return ( ); } - -// Styled components and layout - -const Body = styled(Box)` - &:not(:empty) { - padding: 0 20px; - } -`; - -const Show = styled(Box).attrs<{ visible?: boolean }>({})<{ visible?: boolean }>` - transform: rotate(${p => (p.visible ? 0 : 270)}deg); -`; - -const Collections = styled(Box)` - &:hover { - cursor: pointer; - } -`; diff --git a/apps/ledger-live-mobile/src/screens/Account/NftCollectionsList.tsx b/apps/ledger-live-mobile/src/screens/Account/NftCollectionsList.tsx index 6b2e1ee0d59b..435ade26db8a 100644 --- a/apps/ledger-live-mobile/src/screens/Account/NftCollectionsList.tsx +++ b/apps/ledger-live-mobile/src/screens/Account/NftCollectionsList.tsx @@ -17,7 +17,7 @@ import Button from "~/components/wrappedUi/Button"; import Touchable from "~/components/Touchable"; import SectionTitle from "../WalletCentricSections/SectionTitle"; import useFeature from "@ledgerhq/live-common/featureFlags/useFeature"; -import { useNftGalleryFilter, isThresholdValid } from "@ledgerhq/live-nft-react"; +import { useNftGalleryFilter, getThreshold } from "@ledgerhq/live-nft-react"; const MAX_COLLECTIONS_TO_SHOW = 3; @@ -28,7 +28,7 @@ type Props = { export default function NftCollectionsList({ account }: Props) { useEnv("HIDE_EMPTY_TOKEN_ACCOUNTS"); const nftsFromSimplehashFeature = useFeature("nftsFromSimplehash"); - const thresold = nftsFromSimplehashFeature?.params?.threshold; + const threshold = nftsFromSimplehashFeature?.params?.threshold; const nftsFromSimplehashEnabled = nftsFromSimplehashFeature?.enabled; const { colors } = useTheme(); const { t } = useTranslation(); @@ -38,7 +38,7 @@ export default function NftCollectionsList({ account }: Props) { nftsOwned: account.nfts || [], addresses: account.freshAddress, chains: [account.currency.id], - threshold: isThresholdValid(thresold) ? Number(thresold) : 75, + threshold: getThreshold(threshold), }); const hiddenNftCollections = useSelector(hiddenNftCollectionsSelector); diff --git a/apps/ledger-live-mobile/src/screens/CustomImage/NFTGallerySelector.tsx b/apps/ledger-live-mobile/src/screens/CustomImage/NFTGallerySelector.tsx index dc28ce0c4137..11a7ab520db3 100644 --- a/apps/ledger-live-mobile/src/screens/CustomImage/NFTGallerySelector.tsx +++ b/apps/ledger-live-mobile/src/screens/CustomImage/NFTGallerySelector.tsx @@ -14,7 +14,7 @@ import { BaseComposite, StackNavigatorProps } from "~/components/RootNavigator/t import { CustomImageNavigatorParamList } from "~/components/RootNavigator/types/CustomImageNavigator"; import { TrackScreen } from "~/analytics"; import useFeature from "@ledgerhq/live-common/featureFlags/useFeature"; -import { isThresholdValid, useNftGalleryFilter } from "@ledgerhq/live-nft-react"; +import { getThreshold, useNftGalleryFilter } from "@ledgerhq/live-nft-react"; import { getEnv } from "@ledgerhq/live-env"; const NB_COLUMNS = 2; @@ -34,7 +34,7 @@ const NFTGallerySelector = ({ navigation, route }: NavigationProps) => { const nftsOrdered = useSelector(orderedVisibleNftsSelector, isEqual); const nftsFromSimplehashFeature = useFeature("nftsFromSimplehash"); - const thresold = nftsFromSimplehashFeature?.params?.threshold; + const threshold = nftsFromSimplehashFeature?.params?.threshold; const nftsFromSimplehashEnabled = nftsFromSimplehashFeature?.enabled; const accounts = useSelector(accountsSelector); @@ -52,7 +52,7 @@ const NFTGallerySelector = ({ navigation, route }: NavigationProps) => { nftsOwned: nftsOrdered || [], addresses: addresses, chains: SUPPORTED_NFT_CURRENCIES, - threshold: isThresholdValid(thresold) ? Number(thresold) : 75, + threshold: getThreshold(threshold), }); const nfts = nftsFromSimplehashEnabled ? filteredNfts : nftsOrdered; diff --git a/apps/ledger-live-mobile/src/screens/Nft/NftGallery/index.tsx b/apps/ledger-live-mobile/src/screens/Nft/NftGallery/index.tsx index 6a9037476d34..82478272b6e3 100644 --- a/apps/ledger-live-mobile/src/screens/Nft/NftGallery/index.tsx +++ b/apps/ledger-live-mobile/src/screens/Nft/NftGallery/index.tsx @@ -21,7 +21,7 @@ import { AccountsNavigatorParamList } from "~/components/RootNavigator/types/Acc import InfoModal from "~/modals/Info"; import { notAvailableModalInfo } from "../NftInfoNotAvailable"; import invariant from "invariant"; -import { useNftGalleryFilter, isThresholdValid } from "@ledgerhq/live-nft-react"; +import { useNftGalleryFilter, getThreshold } from "@ledgerhq/live-nft-react"; const MAX_COLLECTIONS_FIRST_RENDER = 12; const COLLECTIONS_TO_ADD_ON_LIST_END_REACHED = 6; @@ -32,7 +32,7 @@ type NavigationProps = BaseComposite< const NftGallery = () => { const nftsFromSimplehashFeature = useFeature("nftsFromSimplehash"); - const thresold = nftsFromSimplehashFeature?.params?.threshold; + const threshold = nftsFromSimplehashFeature?.params?.threshold; const nftsFromSimplehashEnabled = nftsFromSimplehashFeature?.enabled; const navigation = useNavigation(); const { t } = useTranslation(); @@ -55,7 +55,7 @@ const NftGallery = () => { nftsOwned: account.nfts || [], addresses: account.freshAddress, chains: [account.currency.id], - threshold: isThresholdValid(thresold) ? Number(thresold) : 75, + threshold: getThreshold(threshold), }); const hiddenNftCollections = useSelector(hiddenNftCollectionsSelector); diff --git a/apps/ledger-live-mobile/src/screens/Nft/WalletNftGallery/index.tsx b/apps/ledger-live-mobile/src/screens/Nft/WalletNftGallery/index.tsx index 0cbcae827846..682c80317b8b 100644 --- a/apps/ledger-live-mobile/src/screens/Nft/WalletNftGallery/index.tsx +++ b/apps/ledger-live-mobile/src/screens/Nft/WalletNftGallery/index.tsx @@ -9,7 +9,7 @@ import { accountsSelector, filteredNftsSelector, hasNftsSelector } from "~/reduc import isEqual from "lodash/isEqual"; import { galleryChainFiltersSelector } from "~/reducers/nft"; -import { isThresholdValid, useNftGalleryFilter } from "@ledgerhq/live-nft-react"; +import { getThreshold, useNftGalleryFilter } from "@ledgerhq/live-nft-react"; import useFeature from "@ledgerhq/live-common/featureFlags/useFeature"; const WalletNftGallery = () => { @@ -17,7 +17,7 @@ const WalletNftGallery = () => { const hasNFTs = useSelector(hasNftsSelector); const accounts = useSelector(accountsSelector); const nftsFromSimplehashFeature = useFeature("nftsFromSimplehash"); - const thresold = nftsFromSimplehashFeature?.params?.threshold; + const threshold = nftsFromSimplehashFeature?.params?.threshold; const chainFilters = useSelector(galleryChainFiltersSelector); const nftsOwned = useSelector(filteredNftsSelector, isEqual); @@ -44,7 +44,7 @@ const WalletNftGallery = () => { addresses, chains, nftsOwned, - threshold: isThresholdValid(thresold) ? Number(thresold) : 75, + threshold: getThreshold(threshold), }); const useSimpleHash = Boolean(nftsFromSimplehashFeature?.enabled); diff --git a/apps/ledger-live-mobile/src/screens/SendFunds/01b-SelectCollection.tsx b/apps/ledger-live-mobile/src/screens/SendFunds/01b-SelectCollection.tsx index 7a5c57246770..3a00e9b53d50 100644 --- a/apps/ledger-live-mobile/src/screens/SendFunds/01b-SelectCollection.tsx +++ b/apps/ledger-live-mobile/src/screens/SendFunds/01b-SelectCollection.tsx @@ -3,7 +3,7 @@ import React, { useCallback, useMemo, useState, memo } from "react"; import { View, StyleSheet, FlatList, TouchableOpacity, Platform } from "react-native"; import { nftsByCollections } from "@ledgerhq/live-nft"; import { - isThresholdValid, + getThreshold, useNftCollectionMetadata, useNftGalleryFilter, useNftMetadata, @@ -91,7 +91,7 @@ const SendFundsSelectCollection = ({ route }: Props) => { const { account } = params; const { colors } = useTheme(); const nftsFromSimplehashFeature = useFeature("nftsFromSimplehash"); - const thresold = nftsFromSimplehashFeature?.params?.threshold; + const threshold = nftsFromSimplehashFeature?.params?.threshold; const nftsFromSimplehashEnabled = nftsFromSimplehashFeature?.enabled; const hiddenNftCollections = useSelector(hiddenNftCollectionsSelector); @@ -99,7 +99,7 @@ const SendFundsSelectCollection = ({ route }: Props) => { nftsOwned: account.nfts || [], addresses: account.freshAddress, chains: [account.currency.id], - threshold: isThresholdValid(thresold) ? Number(thresold) : 75, + threshold: getThreshold(threshold), }); const [collectionsCount, setCollectionsCount] = useState(MAX_COLLECTIONS_FIRST_RENDER); diff --git a/libs/live-nft-react/src/hooks/helpers/const.ts b/libs/live-nft-react/src/hooks/helpers/const.ts new file mode 100644 index 000000000000..d74658dfd200 --- /dev/null +++ b/libs/live-nft-react/src/hooks/helpers/const.ts @@ -0,0 +1,3 @@ +const DEFAULT_THRESHOLD = 75; + +export { DEFAULT_THRESHOLD }; diff --git a/libs/live-nft-react/src/hooks/helpers/index.ts b/libs/live-nft-react/src/hooks/helpers/index.ts index 55657dcfe669..717b05437ac3 100644 --- a/libs/live-nft-react/src/hooks/helpers/index.ts +++ b/libs/live-nft-react/src/hooks/helpers/index.ts @@ -1,3 +1,5 @@ +import { DEFAULT_THRESHOLD } from "./const"; + export function hashProtoNFT(contract: string, tokenId: string, currencyId: string): string { return `${contract}|${tokenId}|${currencyId}`; } @@ -5,3 +7,12 @@ export function hashProtoNFT(contract: string, tokenId: string, currencyId: stri export function isThresholdValid(threshold?: string | number): boolean { return Number(threshold) >= 0 && Number(threshold) <= 100; } + +export function decodeCollectionId(collectionId: string) { + const [accountId, contractAddress] = collectionId.split("|"); + return { accountId, contractAddress }; +} + +export function getThreshold(threshold?: string | number): number { + return isThresholdValid(threshold) ? Number(threshold) : DEFAULT_THRESHOLD; +} diff --git a/libs/live-nft-react/src/index.ts b/libs/live-nft-react/src/index.ts index bbe9948fc0b9..21922170f011 100644 --- a/libs/live-nft-react/src/index.ts +++ b/libs/live-nft-react/src/index.ts @@ -9,4 +9,5 @@ export * from "./hooks/useFetchOrdinalByTokenId"; export * from "./hooks/helpers/ordinals"; export * from "./hooks/useCheckNftAccount"; export * from "./hooks/helpers/index"; +export * from "./hooks/helpers/const"; export * from "./hooks/types"; diff --git a/libs/live-nft/src/__tests__/index.test.ts b/libs/live-nft/src/__tests__/index.test.ts index 16dc7888f79b..d355c0a1eac0 100644 --- a/libs/live-nft/src/__tests__/index.test.ts +++ b/libs/live-nft/src/__tests__/index.test.ts @@ -3,7 +3,16 @@ import { getCryptoCurrencyById } from "@ledgerhq/cryptoassets"; import { Account, NFTStandard, ProtoNFT } from "@ledgerhq/types-live"; import { encodeNftId } from "@ledgerhq/coin-framework/nft/nftId"; import { genAccount } from "@ledgerhq/coin-framework/mocks/account"; -import { getNFT, getNftCollectionKey, getNftKey, groupByCurrency, orderByLastReceived } from ".."; +import { + getNFT, + getNftCollectionKey, + getNftKey, + groupByCurrency, + mapChain, + mapChains, + orderByLastReceived, +} from ".."; +import { BlockchainEVM } from "../supported"; const NFT_1 = { id: encodeNftId("js:2:0ddkdlsPmds", "contract", "nft.tokenId", "ethereum"), @@ -46,24 +55,35 @@ const accounts: Account[] = [ genAccount("mocked-account-2", { currency: ETH, withNft: true }), ]; -describe("helpers", () => { - it("getNftKey", () => { +describe("Helpers functions", () => { + it("should return Nft key", () => { expect(getNftKey("contract", "tokenId", "currencyId")).toEqual("currencyId-contract-tokenId"); }); - it("getNftCollectionKey", () => { + it("should get NftCollection Key", () => { expect(getNftCollectionKey("contract", "currencyId")).toEqual("currencyId-contract"); }); - it("getNFT", () => { + it("should ge NFT", () => { expect(getNFT("contract", "nft.tokenId", NFTs)).toEqual(NFT_1); }); - it("groupByCurrency", () => { + it("should group by Currency", () => { expect(groupByCurrency(NFTs)).toEqual([NFT_1, NFT_4, NFT_2, NFT_3]); }); - it("orderByLastReceived", () => { + it("should order By last received", () => { expect(orderByLastReceived(accounts, NFTs)).toHaveLength(0); const NFTs_TEST = accounts.map(a => a.nfts).flat() as ProtoNFT[]; expect(orderByLastReceived(accounts, NFTs.concat(NFTs_TEST)).length).toBeGreaterThanOrEqual(1); }); + + it("should map a single chain", () => { + expect(mapChain(BlockchainEVM.Avalanche)).toEqual("avalanche"); + }); + + it("should map all chains", () => { + expect(mapChains([BlockchainEVM.Avalanche, BlockchainEVM.Ethereum])).toEqual([ + "avalanche", + "ethereum", + ]); + }); }); diff --git a/libs/live-nft/src/api/simplehash.ts b/libs/live-nft/src/api/simplehash.ts index 9ba69abfc812..c4df41b4918e 100644 --- a/libs/live-nft/src/api/simplehash.ts +++ b/libs/live-nft/src/api/simplehash.ts @@ -5,8 +5,7 @@ import { SimpleHashSpamReportResponse, } from "./types"; import { getEnv } from "@ledgerhq/live-env"; -import { replacements } from "../supported"; -import { mapChains } from ".."; +import { mapChain, mapChains } from ".."; /** * @@ -79,7 +78,7 @@ const defaultOpts = { export async function fetchNftsFromSimpleHash(opts: NftFetchOpts): Promise { const { chains, addresses, limit, filters, cursor, threshold } = { ...defaultOpts, ...opts }; - const chainsMapped = mapChains(chains, replacements); + const chainsMapped = mapChains(chains); const enrichedFilters = buildFilters(filters, { threshold: String(threshold) }); const { data } = await network({ @@ -139,7 +138,7 @@ export async function reportSpamNtf( }, data: JSON.stringify({ contract_address: opts.contractAddress, - chain_id: opts.chainId, + chain_id: mapChain(opts.chainId), token_id: opts.tokenId, collection_id: opts.collectionId, event_type: opts.eventType, diff --git a/libs/live-nft/src/index.ts b/libs/live-nft/src/index.ts index 501b87d06a04..6165681be904 100644 --- a/libs/live-nft/src/index.ts +++ b/libs/live-nft/src/index.ts @@ -2,6 +2,7 @@ import { getEnv } from "@ledgerhq/live-env"; import { groupAccountsOperationsByDay } from "@ledgerhq/coin-framework/account/index"; import type { Operation, ProtoNFT, NFT, Account } from "@ledgerhq/types-live"; import { NFTResource } from "./types"; +import { replacements } from "./supported"; export const GENESIS_PASS_COLLECTION_CONTRACT = "0x33c6Eec1723B12c46732f7AB41398DE45641Fa42"; export const INFINITY_PASS_COLLECTION_CONTRACT = "0xfe399E9a4B0bE4087a701fF0B1c89dABe7ce5425"; @@ -130,6 +131,11 @@ export const isNftTransaction = (transaction: T | undefined | null): boolean return false; }; -export const mapChains = (chains: string[], replacements: { [key: string]: string }) => { - return chains.map(chain => replacements[chain] || chain); +export const mapChains = (chains: string[]) => { + return chains.map(mapChain); +}; + +export const mapChain = (chain?: string) => { + if (!chain) return; + return replacements[chain] || chain; };