diff --git a/.changeset/nervous-pumpkins-remain.md b/.changeset/nervous-pumpkins-remain.md new file mode 100644 index 000000000000..91ee80c43882 --- /dev/null +++ b/.changeset/nervous-pumpkins-remain.md @@ -0,0 +1,8 @@ +--- +"@ledgerhq/types-live": patch +"ledger-live-desktop": patch +"@ledgerhq/live-common": patch +"@ledgerhq/live-nft-react": patch +--- + +Add useCheckNftAccount Hook diff --git a/.changeset/short-spoons-notice.md b/.changeset/short-spoons-notice.md new file mode 100644 index 000000000000..907408392f60 --- /dev/null +++ b/.changeset/short-spoons-notice.md @@ -0,0 +1,7 @@ +--- +"ledger-live-desktop": patch +"@ledgerhq/live-common": patch +"@ledgerhq/live-nft-react": patch +--- + +use Hook CheckNft in Default and handle global sync of NFTs every 12 hours 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 e22136ad0620..8dfbd271f98d 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 @@ -3,13 +3,12 @@ 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 { useNftGalleryFilter, isThresholdValid, Chain } from "@ledgerhq/live-nft-react"; import { nftsByCollections } from "@ledgerhq/live-nft"; import { useCallback, useEffect, useMemo, 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 { ChainsEnum } from "LLD/features/Collectibles/types/enum/Chains"; const defaultNumberOfVisibleNfts = 10; @@ -32,7 +31,7 @@ const useNftGalleryModel = () => { const { nfts, fetchNextPage, hasNextPage } = useNftGalleryFilter({ nftsOwned: account?.nfts || [], addresses: account?.freshAddress || "", - chains: [account?.currency.id ?? ChainsEnum.ETHEREUM], + chains: [account?.currency.id ?? Chain.ETHEREUM], threshold: isThresholdValid(threshold) ? Number(threshold) : 75, }); diff --git a/apps/ledger-live-desktop/src/renderer/Default.tsx b/apps/ledger-live-desktop/src/renderer/Default.tsx index 0bed65b6e48a..332883c6189b 100644 --- a/apps/ledger-live-desktop/src/renderer/Default.tsx +++ b/apps/ledger-live-desktop/src/renderer/Default.tsx @@ -56,6 +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"; const PlatformCatalog = lazy(() => import("~/renderer/screens/platform")); const Dashboard = lazy(() => import("~/renderer/screens/dashboard")); @@ -202,12 +203,15 @@ export default function Default() { useRecoverRestoreOnboarding(); useAutoDismissPostOnboardingEntryPoint(); + useSyncNFTsWithAccounts(); + const analyticsFF = useFeature("lldAnalyticsOptInPrompt"); const hasSeenAnalyticsOptInPrompt = useSelector(hasSeenAnalyticsOptInPromptSelector); const nftReworked = useFeature("lldNftsGalleryNewArch"); const isLocked = useSelector(isLockedSelector); const dispatch = useDispatch(); const isNftReworkedEnabled = nftReworked?.enabled; + useEffect(() => { if ( !isLocked && 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 74f75add2433..309e5d84c53d 100644 --- a/apps/ledger-live-desktop/src/renderer/components/CustomImage/NFTGallerySelector.tsx +++ b/apps/ledger-live-desktop/src/renderer/components/CustomImage/NFTGallerySelector.tsx @@ -10,8 +10,8 @@ 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 { isThresholdValid, useNftGalleryFilter } from "@ledgerhq/live-nft-react"; import { getEnv } from "@ledgerhq/live-env"; +import { isThresholdValid, useNftGalleryFilter } from "@ledgerhq/live-nft-react"; const ScrollContainer = styled(Flex).attrs({ flexDirection: "column", diff --git a/apps/ledger-live-desktop/src/renderer/hooks/useSyncNFTsWithAccounts.ts b/apps/ledger-live-desktop/src/renderer/hooks/useSyncNFTsWithAccounts.ts new file mode 100644 index 000000000000..695c509772cd --- /dev/null +++ b/apps/ledger-live-desktop/src/renderer/hooks/useSyncNFTsWithAccounts.ts @@ -0,0 +1,88 @@ +import { useEffect, useMemo, useState } from "react"; +import { useHideSpamCollection } from "./useHideSpamCollection"; +import { isThresholdValid, supportedChains, 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 isEqual from "lodash/isEqual"; + +/** + * Represents the size of groups for batching address fetching. + * @constant {number} + */ +const GROUP_SIZE = 20; + +/** + * Represents the timer duration for updating address groups. + * 5 hours = 18,000,000 ms. + * @constant {number} + */ +const TIMER = 5 * 60 * 60 * 1000; // 5 hours = 18000000 ms + +/** + * A React hook that synchronizes NFT accounts by fetching their data in groups. + * It utilizes address batching and manages updates based on a timer. + * + * @returns {void} + * + * @example + * import { useSyncNFTsWithAccounts } from './path/to/hook'; + * + * const MyComponent = () => { + * useSyncNFTsWithAccounts(); + * return
Syncing NFT Accounts...
; + * }; + */ + +export function useSyncNFTsWithAccounts() { + const nftsFromSimplehashFeature = useFeature("nftsFromSimplehash"); + const threshold = isThresholdValid(Number(nftsFromSimplehashFeature?.params?.threshold)) + ? Number(nftsFromSimplehashFeature?.params?.threshold) + : 75; + + const { enabled, hideSpamCollection } = useHideSpamCollection(); + + const accounts = useSelector(accountsSelector); + const nftsOwned = useSelector(orderedVisibleNftsSelector, isEqual); + + const addressGroups = useMemo(() => { + const uniqueAddresses = [ + ...new Set( + accounts.map(account => account.freshAddress).filter(addr => addr.startsWith("0x")), + ), + ]; + + return uniqueAddresses.reduce((acc, _, i, arr) => { + if (i % GROUP_SIZE === 0) { + acc.push(arr.slice(i, i + GROUP_SIZE)); + } + return acc; + }, []); + }, [accounts]); + + const [groupToFetch, setGroupToFetch] = useState(addressGroups[0]); + const [, setCurrentIndex] = useState(0); + + 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: supportedChains, + threshold, + action: hideSpamCollection, + enabled, + }); +} 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 2659c6c90afc..6c2fd6ba14b1 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 @@ -20,7 +20,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 { Chain, isThresholdValid, useNftGalleryFilter } from "@ledgerhq/live-nft-react"; import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; const SpinnerContainer = styled.div` @@ -69,7 +69,7 @@ const Gallery = () => { const { nfts, fetchNextPage, hasNextPage } = useNftGalleryFilter({ nftsOwned: account?.nfts || [], addresses: account?.freshAddress || "", - chains: [account?.currency.id ?? "ethereum"], + chains: [account?.currency.id ?? Chain.ETHEREUM], threshold: isThresholdValid(thresold) ? Number(thresold) : 75, }); diff --git a/libs/ledger-live-common/src/market/utils/timers.ts b/libs/ledger-live-common/src/market/utils/timers.ts index 2e9bac6e03b4..1a01e24cbb1f 100644 --- a/libs/ledger-live-common/src/market/utils/timers.ts +++ b/libs/ledger-live-common/src/market/utils/timers.ts @@ -3,3 +3,4 @@ export const REFETCH_TIME_ONE_MINUTE = 60 * 1000; export const BASIC_REFETCH = 3; // nb minutes export const ONE_DAY = 24 * 60 * 60 * 1000; +export const HALF_DAY = ONE_DAY / 2; diff --git a/libs/live-nft-react/src/hooks/types.ts b/libs/live-nft-react/src/hooks/types.ts index 8a33820aa55c..051f88385404 100644 --- a/libs/live-nft-react/src/hooks/types.ts +++ b/libs/live-nft-react/src/hooks/types.ts @@ -19,6 +19,7 @@ export type HookProps = { chains: string[]; threshold: number; action?: (collection: string) => void; + enabled?: boolean; }; export type PartialProtoNFT = Partial; diff --git a/libs/live-nft-react/src/hooks/useCheckNftAccount.ts b/libs/live-nft-react/src/hooks/useCheckNftAccount.ts index 4eaad5874cbd..6d67b3f56ef4 100644 --- a/libs/live-nft-react/src/hooks/useCheckNftAccount.ts +++ b/libs/live-nft-react/src/hooks/useCheckNftAccount.ts @@ -9,19 +9,32 @@ import { nftsByCollections } from "@ledgerhq/live-nft/index"; import { hashProtoNFT } from "./helpers"; /** - * useCheckNftAccount() will apply a spam filtering on top of existing NFT data. - * - addresses: a list of wallet addresses separated by a "," - * - nftOwned: the array of all nfts as found by all user's account on Ledger Live - * - chains: a list of selected network to search for NFTs - * - action: custom action to handle collections - * NB: for performance, make sure that addresses, nftOwned and chains are memoized + * A React hook that checks NFT accounts against specified criteria and provides filtering functionality for managing NFT collections. + * + * @param {Object} params - The parameters for the hook. + * @param {string} params.addresses - A comma-separated string of NFT addresses to check. + * @param {Array} params.nftsOwned - An array of owned NFTs. + * @param {Array} params.chains - An array representing the blockchain chains. + * @param {number} params.threshold - A numeric threshold for filtering NFTs. + * @param {Function} params.action - A callback function to execute when spam is detected. + * @param {boolean} [params.enabled=false] - A flag to enable or disable the hook's functionality. + * + * @returns {Object} The result of the hook. + * @returns {Array} returns.nfts - An array of filtered NFTs. + * @returns {Object} returns.queryResult - The result of the infinite query, containing pagination and loading states. + * */ + +export const ONE_DAY = 24 * 60 * 60 * 1000; +export const HALF_DAY = ONE_DAY / 2; + export function useCheckNftAccount({ addresses, nftsOwned, chains, threshold, action, + enabled, }: HookProps): NftsFilterResult { // for performance, we hashmap the list of nfts by hash. const nftsWithProperties = useMemo( @@ -36,7 +49,9 @@ export function useCheckNftAccount({ fetchNftsFromSimpleHash({ addresses, chains, cursor: pageParam, threshold }), initialPageParam: undefined, getNextPageParam: lastPage => lastPage.next_cursor, - enabled: addresses.length > 0, + enabled: enabled && addresses.length > 0, + refetchInterval: HALF_DAY, + staleTime: HALF_DAY, }); useEffect(() => { diff --git a/libs/live-nft-react/src/index.ts b/libs/live-nft-react/src/index.ts index bbe9948fc0b9..09f4ee858db1 100644 --- a/libs/live-nft-react/src/index.ts +++ b/libs/live-nft-react/src/index.ts @@ -10,3 +10,4 @@ export * from "./hooks/helpers/ordinals"; export * from "./hooks/useCheckNftAccount"; export * from "./hooks/helpers/index"; export * from "./hooks/types"; +export * from "./supported"; diff --git a/libs/live-nft-react/src/supported.ts b/libs/live-nft-react/src/supported.ts new file mode 100644 index 000000000000..309d048b39dd --- /dev/null +++ b/libs/live-nft-react/src/supported.ts @@ -0,0 +1,6 @@ +export enum Chain { + ETHEREUM = "ethereum", + POLYGON = "polygon", +} + +export const supportedChains = [Chain.ETHEREUM, Chain.POLYGON];