diff --git a/.changeset/dull-geckos-remain.md b/.changeset/dull-geckos-remain.md new file mode 100644 index 000000000000..30405ac2464d --- /dev/null +++ b/.changeset/dull-geckos-remain.md @@ -0,0 +1,8 @@ +--- +"ledger-live-desktop": patch +"live-mobile": patch +"@ledgerhq/live-nft": patch +"@ledgerhq/live-env": patch +--- + +Add new EVMs to NFT support 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 0fa6f47a36f4..74f75add2433 100644 --- a/apps/ledger-live-desktop/src/renderer/components/CustomImage/NFTGallerySelector.tsx +++ b/apps/ledger-live-desktop/src/renderer/components/CustomImage/NFTGallerySelector.tsx @@ -11,6 +11,7 @@ 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"; const ScrollContainer = styled(Flex).attrs({ flexDirection: "column", @@ -30,6 +31,7 @@ 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); @@ -52,7 +54,7 @@ const NFTGallerySelector = ({ handlePickNft, selectedNftId }: Props) => { } = useNftGalleryFilter({ nftsOwned: nftsOrdered || [], addresses: addresses, - chains: ["ethereum", "polygon"], + chains: SUPPORTED_NFT_CURRENCIES, threshold: isThresholdValid(threshold) ? Number(threshold) : 75, }); diff --git a/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Developer/SimpleHashTools/SpamReportNtf/index.tsx b/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Developer/SimpleHashTools/SpamReportNtf/index.tsx index 6590ce606728..3e257bc57ed5 100644 --- a/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Developer/SimpleHashTools/SpamReportNtf/index.tsx +++ b/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Developer/SimpleHashTools/SpamReportNtf/index.tsx @@ -9,6 +9,7 @@ import { ReportOption } from "./type"; import { SpamReportNftResult } from "@ledgerhq/live-nft-react/hooks/types"; import styled from "styled-components"; import { Result } from "../components/Result"; +import { createOptions } from "../helper"; export type HookResult = { handleCollectionIdChange: (value: string) => void; @@ -167,10 +168,7 @@ export default function SpamReport(props: HookResult) { title={t("settings.developer.debugSimpleHash.debugSpamNft.chainId")} desc={t("settings.developer.debugSimpleHash.debugSpamNft.chainIdDesc")} value={{ label: chainId, value: chainId }} - options={[ - { label: "Ethereum", value: "ethereum" }, - { label: "Polygon", value: "polygon" }, - ]} + options={createOptions()} onChange={handleChainIdChange} /> diff --git a/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Developer/SimpleHashTools/SpamScore/index.tsx b/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Developer/SimpleHashTools/SpamScore/index.tsx index f30de51066aa..76bc9e4cf834 100644 --- a/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Developer/SimpleHashTools/SpamScore/index.tsx +++ b/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Developer/SimpleHashTools/SpamScore/index.tsx @@ -8,6 +8,7 @@ import { CheckSpamScoreResult } from "@ledgerhq/live-nft-react/hooks/types"; import { Result } from "../components/Result"; import { LedgerAPI4xx } from "@ledgerhq/errors"; import { SimpleHashResponse } from "@ledgerhq/live-nft/api/types"; +import { createOptions } from "../helper"; export type HookResult = { checkSpamScore: CheckSpamScoreResult; @@ -104,10 +105,7 @@ export default function SpamScore(props: HookResult) { title={t("settings.developer.debugSimpleHash.debugRefreshMetadata.chainId")} desc={t("settings.developer.debugSimpleHash.debugRefreshMetadata.chainIdDesc")} value={{ label: chainId, value: chainId }} - options={[ - { label: "Ethereum", value: "ethereum" }, - { label: "Polygon", value: "polygon" }, - ]} + options={createOptions()} onChange={handleChainIdChange} /> diff --git a/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Developer/SimpleHashTools/helper.ts b/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Developer/SimpleHashTools/helper.ts new file mode 100644 index 000000000000..70494c027a50 --- /dev/null +++ b/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Developer/SimpleHashTools/helper.ts @@ -0,0 +1,8 @@ +import { SUPPORTED_BLOCKCHAINS_LIVE } from "@ledgerhq/live-nft/supported"; + +export const createOptions = () => { + return SUPPORTED_BLOCKCHAINS_LIVE.map(blockchain => ({ + label: blockchain.charAt(0).toUpperCase() + blockchain.slice(1), // Capitalize first letter + value: blockchain, + })); +}; diff --git a/apps/ledger-live-mobile/src/components/Nft/NftGallery/NftFilterDrawer/index.tsx b/apps/ledger-live-mobile/src/components/Nft/NftGallery/NftFilterDrawer/index.tsx index 2ee43378e8b2..ef0a0bb341ce 100644 --- a/apps/ledger-live-mobile/src/components/Nft/NftGallery/NftFilterDrawer/index.tsx +++ b/apps/ledger-live-mobile/src/components/Nft/NftGallery/NftFilterDrawer/index.tsx @@ -10,6 +10,7 @@ import { useNavigation } from "@react-navigation/native"; import { track } from "~/analytics"; import { View } from "react-native"; import QueuedDrawer from "../../../QueuedDrawer"; +import { CryptoCurrencyId } from "@ledgerhq/types-cryptoassets"; type Props = { isOpen: boolean; @@ -42,7 +43,7 @@ const NftFilterDraw: FC = ({ onClose, isOpen, filters, toggleFilter }) => return ( { track("button_clicked", { diff --git a/apps/ledger-live-mobile/src/reducers/nft.ts b/apps/ledger-live-mobile/src/reducers/nft.ts index 9efbffebcb74..1f446ccae73d 100644 --- a/apps/ledger-live-mobile/src/reducers/nft.ts +++ b/apps/ledger-live-mobile/src/reducers/nft.ts @@ -7,13 +7,17 @@ import type { NftPayload, } from "../actions/types"; import { NftStateActionTypes } from "../actions/types"; +import { SUPPORTED_BLOCKCHAINS_LIVE, SupportedBlockchainsType } from "@ledgerhq/live-nft/supported"; export const INITIAL_STATE: NftState = { filterDrawerVisible: false, - galleryChainFilters: { - ethereum: true, - polygon: true, - }, + galleryChainFilters: SUPPORTED_BLOCKCHAINS_LIVE.reduce( + (filters, chain) => { + filters[chain] = true; + return filters; + }, + {} as Record, + ), }; const handlers: ReducerMap = { diff --git a/apps/ledger-live-mobile/src/reducers/types.ts b/apps/ledger-live-mobile/src/reducers/types.ts index 015b5927cfc1..7dc9c9aae716 100644 --- a/apps/ledger-live-mobile/src/reducers/types.ts +++ b/apps/ledger-live-mobile/src/reducers/types.ts @@ -8,7 +8,7 @@ import type { } from "@ledgerhq/types-live"; import type { Device } from "@ledgerhq/live-common/hw/actions/types"; import type { DeviceModelId } from "@ledgerhq/devices"; -import type { CryptoCurrencyId, Currency, Unit } from "@ledgerhq/types-cryptoassets"; +import type { Currency, Unit } from "@ledgerhq/types-cryptoassets"; import { MarketListRequestParams } from "@ledgerhq/live-common/market/utils/types"; import { PostOnboardingState } from "@ledgerhq/types-live"; import { AvailableProviderV3, ExchangeRate } from "@ledgerhq/live-common/exchange/swap/types"; @@ -30,6 +30,7 @@ import { CLSSupportedDeviceModelId } from "@ledgerhq/live-common/device/use-case import { WalletState } from "@ledgerhq/live-wallet/store"; import { TrustchainStore } from "@ledgerhq/ledger-key-ring-protocol/store"; import { Steps } from "LLM/features/WalletSync/types/Activation"; +import { SupportedBlockchainsType, BlockchainsType } from "@ledgerhq/live-nft/supported"; // === ACCOUNT STATE === @@ -332,8 +333,8 @@ export type NftState = { }; export type NftGalleryChainFiltersState = Pick< - Record, - "polygon" | "ethereum" + Record, + SupportedBlockchainsType >; // === MARKET STATE === diff --git a/apps/ledger-live-mobile/src/screens/CustomImage/NFTGallerySelector.tsx b/apps/ledger-live-mobile/src/screens/CustomImage/NFTGallerySelector.tsx index 06e8fa17d6db..dc28ce0c4137 100644 --- a/apps/ledger-live-mobile/src/screens/CustomImage/NFTGallerySelector.tsx +++ b/apps/ledger-live-mobile/src/screens/CustomImage/NFTGallerySelector.tsx @@ -15,6 +15,7 @@ import { CustomImageNavigatorParamList } from "~/components/RootNavigator/types/ import { TrackScreen } from "~/analytics"; import useFeature from "@ledgerhq/live-common/featureFlags/useFeature"; import { isThresholdValid, useNftGalleryFilter } from "@ledgerhq/live-nft-react"; +import { getEnv } from "@ledgerhq/live-env"; const NB_COLUMNS = 2; @@ -28,6 +29,8 @@ const NFTGallerySelector = ({ navigation, route }: NavigationProps) => { const { params } = route; const { device, deviceModelId } = params; + const SUPPORTED_NFT_CURRENCIES = getEnv("NFT_CURRENCIES"); + const nftsOrdered = useSelector(orderedVisibleNftsSelector, isEqual); const nftsFromSimplehashFeature = useFeature("nftsFromSimplehash"); @@ -48,7 +51,7 @@ const NFTGallerySelector = ({ navigation, route }: NavigationProps) => { const { nfts: filteredNfts, isLoading } = useNftGalleryFilter({ nftsOwned: nftsOrdered || [], addresses: addresses, - chains: ["ethereum", "polygon"], + chains: SUPPORTED_NFT_CURRENCIES, threshold: isThresholdValid(thresold) ? Number(thresold) : 75, }); diff --git a/libs/coin-framework/src/nft/support.ts b/libs/coin-framework/src/nft/support.ts index 315a59795a0f..63341ac2ffb8 100644 --- a/libs/coin-framework/src/nft/support.ts +++ b/libs/coin-framework/src/nft/support.ts @@ -10,7 +10,7 @@ import { CollectionMetadataInput, NftMetadataInput, NftRequestsBatcher } from ". import { makeBatcher } from "@ledgerhq/live-network/batcher"; export function isNFTActive(currency: CryptoCurrency | undefined | null): boolean { - return !!currency && getEnv("NFT_CURRENCIES").split(",").includes(currency?.id); + return !!currency && getEnv("NFT_CURRENCIES").includes(currency?.id); } const nftCapabilities: Record = { diff --git a/libs/coin-modules/coin-evm/src/__tests__/unit/logic.unit.test.ts b/libs/coin-modules/coin-evm/src/__tests__/unit/logic.unit.test.ts index 6c35b19e648e..56c8d233efb9 100644 --- a/libs/coin-modules/coin-evm/src/__tests__/unit/logic.unit.test.ts +++ b/libs/coin-modules/coin-evm/src/__tests__/unit/logic.unit.test.ts @@ -546,7 +546,7 @@ describe("EVM Family", () => { describe("getSyncHash", () => { const currency = getCryptoCurrencyById("ethereum"); - let oldEnv: string; + let oldEnv: string[]; beforeAll(() => { oldEnv = getEnv("NFT_CURRENCIES"); }); @@ -569,9 +569,9 @@ describe("EVM Family", () => { }); it("should provide a new hash if nft support is activated or not", () => { - setEnv("NFT_CURRENCIES", ""); + setEnv("NFT_CURRENCIES", []); const hash1 = getSyncHash(currency); - setEnv("NFT_CURRENCIES", currency.id); + setEnv("NFT_CURRENCIES", [currency.id]); const hash2 = getSyncHash(currency); expect(hash1).not.toEqual(hash2); diff --git a/libs/env/src/env.ts b/libs/env/src/env.ts index 1d9d9fe2fa28..176d733c707b 100644 --- a/libs/env/src/env.ts +++ b/libs/env/src/env.ts @@ -537,8 +537,8 @@ const envDefinitions = { desc: "if defined, avoids bypass of the currentDevice in the store.", }, NFT_CURRENCIES: { - def: "ethereum,polygon", - parser: stringParser, + def: ["arbitrum", "avalanche_c_chain", "base", "ethereum", "optimism", "polygon", "scroll"], + parser: stringArrayParser, desc: "set the currencies where NFT is active", }, NFT_ETH_METADATA_SERVICE: { diff --git a/libs/live-nft/README.md b/libs/live-nft/README.md new file mode 100644 index 000000000000..5563d46c2455 --- /dev/null +++ b/libs/live-nft/README.md @@ -0,0 +1,12 @@ +### NFT Support for EVM + +To add an EVM (currently), you need to modify: + +- NFT_CURRENCIES in live-env (CryptoCurrencyId) +- Add it to the BlockchainEVM enum in ./src/supported.ts +- Add the new entry to SUPPORTED_BLOCKCHAINS_LIVE +- If the currencyId is different between LL and SimpleHash, add it to replacements with the corresponding value + +⚠️ When adding a new EVM, **be careful** with the NFT Gallery on LLM: + +Ensure that the filters still work properly diff --git a/libs/live-nft/src/index.test.ts b/libs/live-nft/src/__tests__/index.test.ts similarity index 98% rename from libs/live-nft/src/index.test.ts rename to libs/live-nft/src/__tests__/index.test.ts index c02a8f271df6..16dc7888f79b 100644 --- a/libs/live-nft/src/index.test.ts +++ b/libs/live-nft/src/__tests__/index.test.ts @@ -3,7 +3,7 @@ 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, orderByLastReceived } from ".."; const NFT_1 = { id: encodeNftId("js:2:0ddkdlsPmds", "contract", "nft.tokenId", "ethereum"), diff --git a/libs/live-nft/src/api/simplehash.ts b/libs/live-nft/src/api/simplehash.ts index ddad4d81a658..9ba69abfc812 100644 --- a/libs/live-nft/src/api/simplehash.ts +++ b/libs/live-nft/src/api/simplehash.ts @@ -5,6 +5,8 @@ import { SimpleHashSpamReportResponse, } from "./types"; import { getEnv } from "@ledgerhq/live-env"; +import { replacements } from "../supported"; +import { mapChains } from ".."; /** * @@ -76,10 +78,13 @@ const defaultOpts = { */ export async function fetchNftsFromSimpleHash(opts: NftFetchOpts): Promise { const { chains, addresses, limit, filters, cursor, threshold } = { ...defaultOpts, ...opts }; + + const chainsMapped = mapChains(chains, replacements); + const enrichedFilters = buildFilters(filters, { threshold: String(threshold) }); const { data } = await network({ method: "GET", - url: `${getEnv("SIMPLE_HASH_API_BASE")}/nfts/owners_v2?chains=${chains.join( + url: `${getEnv("SIMPLE_HASH_API_BASE")}/nfts/owners_v2?chains=${chainsMapped.join( ",", )}&wallet_addresses=${addresses}&limit=${limit}${filters ? enrichedFilters : ""}${ cursor ? `&cursor=${cursor}` : "" diff --git a/libs/live-nft/src/index.ts b/libs/live-nft/src/index.ts index 0f799d228548..501b87d06a04 100644 --- a/libs/live-nft/src/index.ts +++ b/libs/live-nft/src/index.ts @@ -3,6 +3,9 @@ import { groupAccountsOperationsByDay } from "@ledgerhq/coin-framework/account/i import type { Operation, ProtoNFT, NFT, Account } from "@ledgerhq/types-live"; import { NFTResource } from "./types"; +export const GENESIS_PASS_COLLECTION_CONTRACT = "0x33c6Eec1723B12c46732f7AB41398DE45641Fa42"; +export const INFINITY_PASS_COLLECTION_CONTRACT = "0xfe399E9a4B0bE4087a701fF0B1c89dABe7ce5425"; + /** * Helper to group NFTs by their collection/contract. * @@ -46,8 +49,8 @@ export const getNFT = ( export const groupByCurrency = (nfts: ProtoNFT[]): ProtoNFT[] => { const groupMap = new Map(); - const SUPPORTED_CURRENCIES = getEnv("NFT_CURRENCIES").split(","); - SUPPORTED_CURRENCIES.forEach(elem => groupMap.set(elem, [])); + const SUPPORTED_NFT_CURRENCIES = getEnv("NFT_CURRENCIES"); + SUPPORTED_NFT_CURRENCIES.forEach(elem => groupMap.set(elem, [])); // GROUPING nfts.forEach(nft => { @@ -91,9 +94,6 @@ export function orderByLastReceived(accounts: Account[], nfts: ProtoNFT[]): Prot return groupByCurrency([...new Set(orderedNFTs)]); } -export const GENESIS_PASS_COLLECTION_CONTRACT = "0x33c6Eec1723B12c46732f7AB41398DE45641Fa42"; -export const INFINITY_PASS_COLLECTION_CONTRACT = "0xfe399E9a4B0bE4087a701fF0B1c89dABe7ce5425"; - export const hasNftInAccounts = (nftCollection: string, accounts: Account[]): boolean => accounts && accounts.some(account => account?.nfts?.some((nft: ProtoNFT) => nft?.contract === nftCollection)); @@ -129,3 +129,7 @@ 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); +}; diff --git a/libs/live-nft/src/supported.ts b/libs/live-nft/src/supported.ts new file mode 100644 index 000000000000..684484300983 --- /dev/null +++ b/libs/live-nft/src/supported.ts @@ -0,0 +1,62 @@ +/** + * Supported blockchains EVM by Backend + */ +export enum BlockchainEVM { + Arbitrum = "arbitrum", + Avalanche = "avalanche_c_chain", + Base = "base", + Blast = "blast", + Bsc = "bsc", + Canto = "canto", + Celo = "celo", + Cyber = "cyber", + Degen = "degen", + Ethereum = "ethereum", + Fantom = "fantom", + Gnosis = "gnosis", + Godwoken = "godwoken", + Linea = "linea", + Loot = "loot", + Manta = "manta", + Mode = "mode", + Moonbeam = "moonbeam", + Opbnb = "opbnb", + Optimism = "optimism", + Palm = "palm", + Polygon = "polygon", + ProofOfPlay = "proofofplay", + Rari = "rari", + Scroll = "scroll", + Sei = "sei_network", + Xai = "xai", + Zora = "zora", +} + +export const blockchainEVMList: BlockchainEVM[] = Object.values(BlockchainEVM); + +export const SUPPORTED_BLOCKCHAINS_LIVE = [ + BlockchainEVM.Arbitrum, + BlockchainEVM.Avalanche, + BlockchainEVM.Base, + // BlockchainEVM.Blast, + // BlockchainEVM.Bsc, + // BlockchainEVM.Celo, + BlockchainEVM.Ethereum, + // BlockchainEVM.Fantom, + // BlockchainEVM.Linea, + // BlockchainEVM.Moonbeam, + BlockchainEVM.Optimism, + BlockchainEVM.Polygon, + BlockchainEVM.Scroll, + // BlockchainEVM.Sei, +]; + +export type BlockchainsType = (typeof blockchainEVMList)[number]; + +export type SupportedBlockchainsType = (typeof SUPPORTED_BLOCKCHAINS_LIVE)[number]; + +// For SimpleHash Api +export const replacements: { [key: string]: string } = { + [BlockchainEVM.Avalanche]: "avalanche", + [BlockchainEVM.Sei]: "sei", +};