Skip to content

Commit

Permalink
support: Add new EVMs to Support NFTs (#7926)
Browse files Browse the repository at this point in the history
* [WIP]: Support new Evms nfts

* support: Add new EVMs to Support NFTs

* chore: Activate partially new EVMs
  • Loading branch information
mcayuelas-ledger authored Oct 22, 2024
1 parent 9834c5e commit 5c13c7b
Show file tree
Hide file tree
Showing 17 changed files with 137 additions and 31 deletions.
8 changes: 8 additions & 0 deletions .changeset/dull-geckos-remain.md
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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);
Expand All @@ -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,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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}
/>
</DisabledContainer>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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}
/>
</Flex>
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
}));
};
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -42,7 +43,7 @@ const NftFilterDraw: FC<Props> = ({ onClose, isOpen, filters, toggleFilter }) =>
return (
<NftFilterCurrencyItem
key={key}
currency={currencyId}
currency={currencyId as CryptoCurrencyId}
isSelected={value}
onPress={() => {
track("button_clicked", {
Expand Down
12 changes: 8 additions & 4 deletions apps/ledger-live-mobile/src/reducers/nft.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<SupportedBlockchainsType, boolean>,
),
};

const handlers: ReducerMap<NftState, NftPayload> = {
Expand Down
7 changes: 4 additions & 3 deletions apps/ledger-live-mobile/src/reducers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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 ===

Expand Down Expand Up @@ -332,8 +333,8 @@ export type NftState = {
};

export type NftGalleryChainFiltersState = Pick<
Record<CryptoCurrencyId, boolean>,
"polygon" | "ethereum"
Record<BlockchainsType, boolean>,
SupportedBlockchainsType
>;

// === MARKET STATE ===
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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");
Expand All @@ -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,
});

Expand Down
2 changes: 1 addition & 1 deletion libs/coin-framework/src/nft/support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, NFTStandard[]> = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -546,7 +546,7 @@ describe("EVM Family", () => {
describe("getSyncHash", () => {
const currency = getCryptoCurrencyById("ethereum");

let oldEnv: string;
let oldEnv: string[];
beforeAll(() => {
oldEnv = getEnv("NFT_CURRENCIES");
});
Expand All @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions libs/env/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
12 changes: 12 additions & 0 deletions libs/live-nft/README.md
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
7 changes: 6 additions & 1 deletion libs/live-nft/src/api/simplehash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
SimpleHashSpamReportResponse,
} from "./types";
import { getEnv } from "@ledgerhq/live-env";
import { replacements } from "../supported";
import { mapChains } from "..";

/**
*
Expand Down Expand Up @@ -76,10 +78,13 @@ const defaultOpts = {
*/
export async function fetchNftsFromSimpleHash(opts: NftFetchOpts): Promise<SimpleHashResponse> {
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<SimpleHashResponse>({
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}` : ""
Expand Down
14 changes: 9 additions & 5 deletions libs/live-nft/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -46,8 +49,8 @@ export const getNFT = (

export const groupByCurrency = (nfts: ProtoNFT[]): ProtoNFT[] => {
const groupMap = new Map<string, ProtoNFT[]>();
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 => {
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -129,3 +129,7 @@ export const isNftTransaction = <T>(transaction: T | undefined | null): boolean

return false;
};

export const mapChains = (chains: string[], replacements: { [key: string]: string }) => {
return chains.map(chain => replacements[chain] || chain);
};
62 changes: 62 additions & 0 deletions libs/live-nft/src/supported.ts
Original file line number Diff line number Diff line change
@@ -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",
};

0 comments on commit 5c13c7b

Please sign in to comment.