-
Notifications
You must be signed in to change notification settings - Fork 326
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: useCheckNftAccount in [LIB] (#7891)
* feat: useCheckNftAccount in [LIB] * ✨(lld): add suggestion * ✨(lld): fix what rebase broke --------- Co-authored-by: Lucas Werey <[email protected]>
- Loading branch information
1 parent
67ed92a
commit 00cab1d
Showing
14 changed files
with
259 additions
and
50 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
--- | ||
"@ledgerhq/types-live": patch | ||
"ledger-live-desktop": patch | ||
"@ledgerhq/live-common": patch | ||
"@ledgerhq/live-nft-react": patch | ||
--- | ||
|
||
Add useCheckNftAccount Hook |
24 changes: 24 additions & 0 deletions
24
apps/ledger-live-desktop/src/renderer/hooks/useHideSpamCollection.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { useCallback } from "react"; | ||
import { useDispatch, useSelector } from "react-redux"; | ||
import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; | ||
import { hideNftCollection } from "~/renderer/actions/settings"; | ||
import { hiddenNftCollectionsSelector } from "../reducers/settings"; | ||
|
||
export function useHideSpamCollection() { | ||
const spamFilteringTxFeature = useFeature("spamFilteringTx"); | ||
const hiddenNftCollections = useSelector(hiddenNftCollectionsSelector); | ||
const dispatch = useDispatch(); | ||
const hideSpamCollection = useCallback( | ||
(collection: string) => { | ||
if (!hiddenNftCollections.includes(collection)) { | ||
dispatch(hideNftCollection(collection)); | ||
} | ||
}, | ||
[dispatch, hiddenNftCollections], | ||
); | ||
|
||
return { | ||
hideSpamCollection, | ||
enabled: spamFilteringTxFeature?.enabled, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
{ | ||
"entry": ["src/index.ts", "src/tools/*", "src/hooks/*"], | ||
"ignoreUnused": [] | ||
"ignoreUnused": ["@ledgerhq/coin-framework/nft/nftId"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
82 changes: 82 additions & 0 deletions
82
libs/live-nft-react/src/hooks/__tests__/useCheckNftAccount.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import { waitFor, renderHook } from "@testing-library/react"; | ||
import { SimpleHashResponse } from "@ledgerhq/live-nft/api/types"; | ||
import { notifyManager } from "@tanstack/react-query"; | ||
|
||
import { wrapper, generateNftsOwned } from "../../tools/helperTests"; | ||
import { useCheckNftAccount } from "../useCheckNftAccount"; | ||
|
||
jest.setTimeout(30000); | ||
|
||
// invoke callback instantly | ||
notifyManager.setScheduler(cb => cb()); | ||
|
||
const pagedBy = 5; | ||
|
||
const nftsOwned = generateNftsOwned(); | ||
const expected = [...new Set(nftsOwned)]; | ||
expected.sort(() => Math.random() - 0.5); | ||
|
||
const apiResults: SimpleHashResponse[] = []; | ||
for (let i = 0; i < expected.length; i += pagedBy) { | ||
const slice = expected.slice(i, i + pagedBy); | ||
const apiResult = { | ||
next_cursor: i + pagedBy < expected.length ? String(i + pagedBy) : null, | ||
nfts: slice.map(nft => ({ | ||
nft_id: nft.id, | ||
chain: "ethereum", | ||
contract_address: nft.contract, | ||
token_id: nft.tokenId, | ||
image_url: "", | ||
name: "", | ||
description: "", | ||
token_count: 1, | ||
collection: { name: "", spam_score: 0 }, | ||
contract: { type: "ERC721" }, | ||
extra_metadata: { | ||
image_original_url: "", | ||
animation_original_url: "", | ||
}, | ||
})), | ||
}; | ||
apiResults.push(apiResult); | ||
} | ||
|
||
let callCount = 0; | ||
|
||
jest.mock("@ledgerhq/live-nft/api/simplehash", () => ({ | ||
fetchNftsFromSimpleHash: jest.fn().mockImplementation(opts => { | ||
const { cursor } = opts; | ||
const index = cursor ? Number(cursor) : 0; | ||
|
||
const pageIndex = Math.floor(index / pagedBy); | ||
if (!apiResults[pageIndex]) throw new Error("no such page"); | ||
|
||
callCount++; | ||
return Promise.resolve(apiResults[pageIndex]); | ||
}), | ||
})); | ||
|
||
describe("useCheckNftAccount", () => { | ||
test("fetches all pages", async () => { | ||
const addresses = "0x34"; | ||
const chains = ["ethereum"]; | ||
|
||
const { result } = renderHook( | ||
() => | ||
useCheckNftAccount({ | ||
addresses, | ||
nftsOwned, | ||
chains, | ||
threshold: 80, | ||
}), | ||
{ | ||
wrapper, | ||
}, | ||
); | ||
|
||
await waitFor(() => !result.current.hasNextPage); | ||
|
||
expect(callCount).toBe(nftsOwned.length / pagedBy); | ||
expect(result.current.nfts.length).toEqual(nftsOwned.length); | ||
}); | ||
}); |
38 changes: 4 additions & 34 deletions
38
libs/live-nft-react/src/hooks/__tests__/useNftGalleryFilter.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
export function hashProtoNFT(contract: string, tokenId: string, currencyId: string): string { | ||
return `${contract}|${tokenId}|${currencyId}`; | ||
} | ||
|
||
export function isThresholdValid(threshold?: string | number): boolean { | ||
return Number(threshold) >= 0 && Number(threshold) <= 100; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
import { useEffect, useMemo } from "react"; | ||
import { useInfiniteQuery } from "@tanstack/react-query"; | ||
import { fetchNftsFromSimpleHash } from "@ledgerhq/live-nft/api/simplehash"; | ||
import { ProtoNFT } from "@ledgerhq/types-live"; | ||
import { NFTS_QUERY_KEY } from "../queryKeys"; | ||
import { HookProps, NftsFilterResult } from "./types"; | ||
import { decodeNftId } from "@ledgerhq/coin-framework/nft/nftId"; | ||
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 | ||
*/ | ||
export function useCheckNftAccount({ | ||
addresses, | ||
nftsOwned, | ||
chains, | ||
threshold, | ||
action, | ||
}: HookProps): NftsFilterResult { | ||
// for performance, we hashmap the list of nfts by hash. | ||
const nftsWithProperties = useMemo( | ||
() => | ||
new Map(nftsOwned.map(obj => [hashProtoNFT(obj.contract, obj.tokenId, obj.currencyId), obj])), | ||
[nftsOwned], | ||
); | ||
|
||
const queryResult = useInfiniteQuery({ | ||
queryKey: [NFTS_QUERY_KEY.SpamFilter, addresses, chains], | ||
queryFn: ({ pageParam }: { pageParam: string | undefined }) => | ||
fetchNftsFromSimpleHash({ addresses, chains, cursor: pageParam, threshold }), | ||
initialPageParam: undefined, | ||
getNextPageParam: lastPage => lastPage.next_cursor, | ||
enabled: addresses.length > 0, | ||
}); | ||
|
||
useEffect(() => { | ||
if (queryResult.hasNextPage && !queryResult.isFetchingNextPage) { | ||
queryResult.fetchNextPage(); | ||
} | ||
}, [queryResult, queryResult.hasNextPage, queryResult.isFetchingNextPage]); | ||
|
||
const out = useMemo(() => { | ||
const nfts: ProtoNFT[] = []; | ||
|
||
const processingNFTs = queryResult.data?.pages.flatMap(page => page.nfts); | ||
|
||
if (!queryResult.hasNextPage && processingNFTs) { | ||
for (const nft of processingNFTs) { | ||
const hash = hashProtoNFT(nft.contract_address, nft.token_id, nft.chain); | ||
const existing = nftsWithProperties.get(hash); | ||
if (existing) { | ||
nfts.push(existing); | ||
} | ||
} | ||
|
||
if (action) { | ||
const spams = nftsOwned.filter(nft => !nfts.some(ownedNft => ownedNft.id === nft.id)); | ||
|
||
const collections = nftsByCollections(spams); | ||
|
||
Object.entries(collections).map(([contract, nfts]: [string, ProtoNFT[]]) => { | ||
const { accountId } = decodeNftId(nfts[0].id); | ||
const collection = `${accountId}|${contract}`; | ||
action(collection); | ||
}); | ||
} | ||
} | ||
return { ...queryResult, nfts }; | ||
}, [queryResult, action, nftsWithProperties, nftsOwned]); | ||
|
||
return out; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,41 @@ | ||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; | ||
import React, { ReactNode } from "react"; | ||
import { BigNumber } from "bignumber.js"; | ||
import { NFTs } from "@ledgerhq/coin-framework/mocks/fixtures/nfts"; | ||
import { encodeNftId } from "@ledgerhq/coin-framework/nft/nftId"; | ||
|
||
const queryClient = new QueryClient(); | ||
|
||
export const wrapper = ({ children }: { children: ReactNode }) => ( | ||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider> | ||
); | ||
|
||
export type FakeNFTRaw = { | ||
id: string; | ||
tokenId: string; | ||
amount: BigNumber; | ||
contract: string; | ||
standard: "ERC721"; | ||
currencyId: string; | ||
metadata: undefined; | ||
}; | ||
|
||
export const generateNftsOwned = () => { | ||
const nfts: FakeNFTRaw[] = []; | ||
|
||
NFTs.forEach(nft => { | ||
for (let i = 1; i <= 20; i++) { | ||
nfts.push({ | ||
id: encodeNftId("foo", nft.collection.contract, String(i), "ethereum"), | ||
tokenId: String(i), | ||
amount: new BigNumber(0), | ||
contract: nft.collection.contract, | ||
standard: "ERC721" as const, | ||
currencyId: "ethereum", | ||
metadata: undefined, | ||
}); | ||
} | ||
}); | ||
|
||
return nfts; | ||
}; |
Oops, something went wrong.