From b2198d7b6723b79419853a625823478a341d03bf Mon Sep 17 00:00:00 2001 From: Lucas Werey Date: Tue, 10 Sep 2024 10:12:25 +0200 Subject: [PATCH] :sparkles:feat(ui): add simplehash calls for ordis --- .changeset/dry-parents-breathe.md | 7 + .../Ordinals/components/Error.tsx | 16 + .../Ordinals/components/Icons.tsx | 103 +- .../Ordinals/components/Inscriptions/Item.tsx | 29 + .../components/Inscriptions/helpers.ts | 47 + .../components/Inscriptions/index.tsx | 77 +- .../Inscriptions/useInscriptionsModel.tsx | 28 +- .../Ordinals/components/Loader.tsx | 12 + .../Ordinals/components/RareSats/Item.tsx | 21 +- .../components/RareSats/RowLayout.tsx | 5 - .../components/RareSats/TableHeader.tsx | 2 - .../Ordinals/components/RareSats/helpers.ts | 153 +- .../Ordinals/components/RareSats/index.tsx | 66 +- .../components/RareSats/useRareSatsModel.ts | 20 +- .../Ordinals/screens/Account/index.tsx | 13 +- .../__integration__/bitcoinPage.test.tsx | 36 + .../__integration__/mockedBTCAccount.ts | 54 + .../__integration__/mockedInscriptions.ts | 90 - .../Collectibles/__integration__/shared.tsx | 8 + .../components/Collection/ShowMore.tsx | 5 +- .../Collection/TableRow/IconContainer.tsx | 29 - .../TableRow/IconContainer/ToolTip.tsx | 19 + .../TableRow/IconContainer/index.tsx | 39 + .../components/Collection/TableRow/index.tsx | 15 +- .../Collectibles/hooks/useFetchOrdinals.ts | 18 + .../Collectibles/hooks/useNftLinks.tsx | 2 +- .../Collectibles/types/Collectibles.ts | 8 +- .../features/Collectibles/types/Collection.ts | 2 + .../Collectibles/types/Inscriptions.ts | 12 + .../features/Collectibles/types/Links.ts | 3 - .../features/Collectibles/types/Ordinals.ts | 21 +- .../features/Collectibles/types/RareSats.ts | 8 - .../Collectibles/types/enum/Collectibles.ts | 8 + .../Collectibles/types/enum/DetailDrawer.ts | 1 + .../Collectibles/utils/typeGuardsChecker.ts | 13 +- .../src/renderer/screens/account/index.tsx | 8 +- .../static/i18n/en/app.json | 33 + .../tests/handlers/fixtures/nfts/index.ts | 2 +- .../tests/handlers/fixtures/ordinals/index.ts | 5 + .../fixtures/ordinals/mockedOrdinals.json | 2575 +++++++++++++++++ .../tests/handlers/nfts.ts | 11 +- libs/ledger-live-common/.unimportedrc.json | 3 +- .../src/account/typeGuards.ts | 10 + .../hooks/__tests__/useFetchOrdinals.test.tsx | 42 + .../src/hooks/helpers/ordinals.ts | 91 + libs/live-nft-react/src/hooks/types.ts | 49 + .../src/hooks/useFetchOrdinals.ts | 49 + libs/live-nft-react/src/index.ts | 2 + libs/live-nft-react/src/queryKeys.ts | 1 + libs/live-nft/src/api/types.ts | 62 + 50 files changed, 3531 insertions(+), 402 deletions(-) create mode 100644 .changeset/dry-parents-breathe.md create mode 100644 apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Error.tsx create mode 100644 apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/Item.tsx create mode 100644 apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/helpers.ts create mode 100644 apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Loader.tsx create mode 100644 apps/ledger-live-desktop/src/newArch/features/Collectibles/__integration__/bitcoinPage.test.tsx create mode 100644 apps/ledger-live-desktop/src/newArch/features/Collectibles/__integration__/mockedBTCAccount.ts delete mode 100644 apps/ledger-live-desktop/src/newArch/features/Collectibles/__integration__/mockedInscriptions.ts delete mode 100644 apps/ledger-live-desktop/src/newArch/features/Collectibles/components/Collection/TableRow/IconContainer.tsx create mode 100644 apps/ledger-live-desktop/src/newArch/features/Collectibles/components/Collection/TableRow/IconContainer/ToolTip.tsx create mode 100644 apps/ledger-live-desktop/src/newArch/features/Collectibles/components/Collection/TableRow/IconContainer/index.tsx create mode 100644 apps/ledger-live-desktop/src/newArch/features/Collectibles/hooks/useFetchOrdinals.ts create mode 100644 apps/ledger-live-desktop/src/newArch/features/Collectibles/types/Inscriptions.ts delete mode 100644 apps/ledger-live-desktop/src/newArch/features/Collectibles/types/Links.ts create mode 100644 apps/ledger-live-desktop/tests/handlers/fixtures/ordinals/index.ts create mode 100644 apps/ledger-live-desktop/tests/handlers/fixtures/ordinals/mockedOrdinals.json create mode 100644 libs/ledger-live-common/src/account/typeGuards.ts create mode 100644 libs/live-nft-react/src/hooks/__tests__/useFetchOrdinals.test.tsx create mode 100644 libs/live-nft-react/src/hooks/helpers/ordinals.ts create mode 100644 libs/live-nft-react/src/hooks/useFetchOrdinals.ts diff --git a/.changeset/dry-parents-breathe.md b/.changeset/dry-parents-breathe.md new file mode 100644 index 000000000000..42c1cc409974 --- /dev/null +++ b/.changeset/dry-parents-breathe.md @@ -0,0 +1,7 @@ +--- +"ledger-live-desktop": patch +"@ledgerhq/live-nft-react": patch +"@ledgerhq/live-nft": patch +--- + +Plug the front with simplehash api for the rare sats table and inscriptions table diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Error.tsx b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Error.tsx new file mode 100644 index 000000000000..6e08b8876426 --- /dev/null +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Error.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import { Flex, Icons, Text } from "@ledgerhq/react-ui"; +import { useTranslation } from "react-i18next"; + +const Error: React.FC = () => { + const { t } = useTranslation(); + + return ( + + + {t("crash.title")} + + ); +}; + +export default Error; diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Icons.tsx b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Icons.tsx index feb7ec041fe5..745bf825fc87 100644 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Icons.tsx +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Icons.tsx @@ -3,71 +3,144 @@ import { Icons } from "@ledgerhq/react-ui"; import { IconProps } from "../../types/Collection"; export const mappingKeysWithIconAndName = { - alpha: { icon: (props: IconProps) => , name: "Alpha" }, + alpha: { + icon: (props: IconProps) => , + name: "Alpha", + descriptionTranslationKey: "ordinals.rareSats.rareSat.description.alpha", + }, black_epic: { icon: (props: IconProps) => , name: "Black Epic", + descriptionTranslationKey: "ordinals.rareSats.rareSat.description.black_epic", }, black_legendary: { icon: (props: IconProps) => , name: "Black Legendary", + descriptionTranslationKey: "ordinals.rareSats.rareSat.description.black_legendary", }, black_mythic: { icon: (props: IconProps) => , name: "Black Mythic", + descriptionTranslationKey: "ordinals.rareSats.rareSat.description.black_mythic", }, black_rare: { icon: (props: IconProps) => , name: "Black Rare", + descriptionTranslationKey: "ordinals.rareSats.rareSat.description.black_rare", }, black_uncommon: { icon: (props: IconProps) => , name: "Black Uncommon", + descriptionTranslationKey: "ordinals.rareSats.rareSat.description.black_uncommon", + }, + block_9: { + icon: (props: IconProps) => , + name: "Block 9", + descriptionTranslationKey: "ordinals.rareSats.rareSat.description.block_9", }, - block_9: { icon: (props: IconProps) => , name: "Block 9" }, block_9_450x: { icon: (props: IconProps) => , name: "Block 9 450x", + descriptionTranslationKey: "ordinals.rareSats.rareSat.description.block_9_450x", + }, + block_78: { + icon: (props: IconProps) => , + name: "Block 78", + descriptionTranslationKey: "ordinals.rareSats.rareSat.description.block_78", }, - block_78: { icon: (props: IconProps) => , name: "Block 78" }, block_286: { icon: (props: IconProps) => , name: "Block 286", + descriptionTranslationKey: "ordinals.rareSats.rareSat.description.block_286", }, block_666: { icon: (props: IconProps) => , name: "Block 666", + descriptionTranslationKey: "ordinals.rareSats.rareSat.description.block_666", + }, + common: { + icon: (props: IconProps) => , + name: "Common", + descriptionTranslationKey: "ordinals.rareSats.rareSat.description.common", + }, + epic: { + icon: (props: IconProps) => , + name: "Epic", + descriptionTranslationKey: "ordinals.rareSats.rareSat.description.epic", }, - common: { icon: (props: IconProps) => , name: "Common" }, - epic: { icon: (props: IconProps) => , name: "Epic" }, first_tx: { icon: (props: IconProps) => , name: "First Transaction", + descriptionTranslationKey: "ordinals.rareSats.rareSat.description.first_tx", + }, + hitman: { + icon: (props: IconProps) => , + name: "Hitman", + descriptionTranslationKey: "ordinals.rareSats.rareSat.description.hitman", + }, + jpeg: { + icon: (props: IconProps) => , + name: "JPEG", + descriptionTranslationKey: "ordinals.rareSats.rareSat.description.jpeg", + }, + legacy: { + icon: (props: IconProps) => , + name: "Legacy", + descriptionTranslationKey: "ordinals.rareSats.rareSat.description.legacy", }, - hitman: { icon: (props: IconProps) => , name: "Hitman" }, - jpeg: { icon: (props: IconProps) => , name: "JPEG" }, - legacy: { icon: (props: IconProps) => , name: "Legacy" }, legendary: { icon: (props: IconProps) => , name: "Legendary", + descriptionTranslationKey: "ordinals.rareSats.rareSat.description.legendary", + }, + mythic: { + icon: (props: IconProps) => , + name: "Mythic", + descriptionTranslationKey: "ordinals.rareSats.rareSat.description.mythic", + }, + nakamoto: { + icon: (props: IconProps) => , + name: "Nakamoto", + descriptionTranslationKey: "ordinals.rareSats.rareSat.description.nakamoto", + }, + omega: { + icon: (props: IconProps) => , + name: "Omega", + descriptionTranslationKey: "ordinals.rareSats.rareSat.description.omega", }, - mythic: { icon: (props: IconProps) => , name: "Mythic" }, - nakamoto: { icon: (props: IconProps) => , name: "Nakamoto" }, - omega: { icon: (props: IconProps) => , name: "Omega" }, paliblock: { icon: (props: IconProps) => , name: "PaliBlock Palindrome", + descriptionTranslationKey: "ordinals.rareSats.rareSat.description.paliblock", }, palindrome: { icon: (props: IconProps) => , name: "Palindrome", + descriptionTranslationKey: "ordinals.rareSats.rareSat.description.palindrome", }, palinception: { icon: (props: IconProps) => , name: "Palinception", + descriptionTranslationKey: "ordinals.rareSats.rareSat.description.palinception", + }, + pizza: { + icon: (props: IconProps) => , + name: "Pizza", + descriptionTranslationKey: "ordinals.rareSats.rareSat.description.pizza", + }, + rare: { + icon: (props: IconProps) => , + name: "Rare", + descriptionTranslationKey: "ordinals.rareSats.rareSat.description.rare", + }, + uncommon: { + icon: (props: IconProps) => , + name: "Uncommon", + descriptionTranslationKey: "ordinals.rareSats.rareSat.description.uncommon", + }, + vintage: { + icon: (props: IconProps) => , + name: "Vintage", + descriptionTranslationKey: "ordinals.rareSats.rareSat.description.vintage", }, - pizza: { icon: (props: IconProps) => , name: "Pizza" }, - rare: { icon: (props: IconProps) => , name: "Rare" }, - uncommon: { icon: (props: IconProps) => , name: "Uncommon" }, - vintage: { icon: (props: IconProps) => , name: "Vintage" }, }; diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/Item.tsx b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/Item.tsx new file mode 100644 index 000000000000..dc3f2ab8ffd8 --- /dev/null +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/Item.tsx @@ -0,0 +1,29 @@ +import { InscriptionsItemProps } from "LLD/features/Collectibles/types/Inscriptions"; +import TableRow from "LLD/features/Collectibles/components/Collection/TableRow"; +import React from "react"; + +type ItemProps = { + isLoading: boolean; +} & InscriptionsItemProps; + +const Item: React.FC = ({ + isLoading, + tokenName, + collectionName, + tokenIcons, + media, + rareSatName, + onClick, +}) => ( + +); + +export default Item; diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/helpers.ts b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/helpers.ts new file mode 100644 index 000000000000..0917bb99b130 --- /dev/null +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/helpers.ts @@ -0,0 +1,47 @@ +import { SimpleHashNft } from "@ledgerhq/live-nft/api/types"; +import { IconProps } from "LLD/features/Collectibles/types/Collection"; +import { mappingKeysWithIconAndName } from "../Icons"; +import { MappingKeys } from "LLD/features/Collectibles/types/Ordinals"; + +function matchCorrespondingIcon( + rareSats: SimpleHashNft[], +): Array JSX.Element> }> { + return rareSats.map(rareSat => { + const iconKeys: string[] = []; + const rarity = rareSat.extra_metadata?.ordinal_details?.sat_rarity?.toLowerCase(); + + if (rarity && rarity !== "common") { + iconKeys.push(rarity.replace(" ", "_")); + } + + const icons = iconKeys + .map( + iconKey => + mappingKeysWithIconAndName[iconKey as keyof typeof mappingKeysWithIconAndName]?.icon, + ) + .filter(Boolean) as Array<({ size, color, style }: IconProps) => JSX.Element>; + + return { ...rareSat, icons }; + }); +} + +export function getInscriptionsData(inscriptions: SimpleHashNft[]) { + const inscriptionsWithIcons = matchCorrespondingIcon(inscriptions); + return inscriptionsWithIcons.map(item => ({ + tokenName: item.name || item.contract.name || "", + collectionName: item.collection.name, + tokenIcons: item.icons, + rareSatName: [item.extra_metadata?.ordinal_details?.sat_rarity] as MappingKeys[], + media: { + uri: item.image_url || item.previews?.image_small_url, + isLoading: false, + useFallback: true, + contentType: item.extra_metadata?.ordinal_details?.content_type, + mediaType: "image", + }, + onClick: () => { + console.log(`you clicked on : \x1b[32m${item.name}\x1b[0m inscription`); + }, + // it does nothing for now but it will be used for the next PR with the drawer + })); +} diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/index.tsx b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/index.tsx index 4691340a2152..489b9f88d905 100644 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/index.tsx +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/index.tsx @@ -1,80 +1,65 @@ import React from "react"; -import { Account } from "@ledgerhq/types-live"; import { Box, Flex } from "@ledgerhq/react-ui"; import { useInscriptionsModel } from "./useInscriptionsModel"; import TableContainer from "~/renderer/components/TableContainer"; import TableHeader from "LLD/features/Collectibles/components/Collection/TableHeader"; -import { OrdinalsRowProps, TableHeaderTitleKey } from "LLD/features/Collectibles/types/Collection"; -import TableRow from "LLD/features/Collectibles/components/Collection/TableRow"; +import { TableHeaderTitleKey } from "LLD/features/Collectibles/types/Collection"; import ShowMore from "LLD/features/Collectibles/components/Collection/ShowMore"; -import { MediaProps } from "LLD/features/Collectibles/types/Media"; +import { SimpleHashNft } from "@ledgerhq/live-nft/api/types"; +import Loader from "../Loader"; +import Error from "../Error"; +import Item from "./Item"; -type ViewProps = ReturnType; +type ViewProps = ReturnType & { isLoading: boolean; isError: boolean }; type Props = { - account: Account; -}; - -export type InscriptionsItemProps = { + inscriptions: SimpleHashNft[]; isLoading: boolean; - tokenName: string; - collectionName: string; - tokenIcons: OrdinalsRowProps["tokenIcons"]; - media: MediaProps; - onClick: () => void; + isError: boolean; }; -const Item: React.FC = ({ +const View: React.FC = ({ + displayShowMore, isLoading, - tokenName, - collectionName, - tokenIcons, - media, - onClick, + isError, + inscriptions, + onShowMore, }) => { - return ( - - ); -}; + const hasInscriptions = inscriptions.length > 0 && !isError; + const nothingToShow = !hasInscriptions && !isLoading && !isError; -function View({ displayedObjects, displayShowMore, onShowMore }: ViewProps) { return ( - + - {/** titlekey doesn't need to be translated so we keep it hard coded */} - {displayedObjects ? ( - displayedObjects.map((item, index) => ( + {isLoading && } + {isError && } + {hasInscriptions && + inscriptions.map((item, index) => ( - )) - ) : ( - - {"NOTHING TO SHOW"} + ))} + {nothingToShow && ( + + {"NOTHING TO SHOW WAITING FOR DESIGN"} )} - {displayShowMore && } + {displayShowMore && !isError && } ); -} - -const Inscriptions: React.FC = ({ account }: Props) => { - return ; }; +const Inscriptions: React.FC = ({ inscriptions, isLoading, isError }) => ( + +); + export default Inscriptions; diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/useInscriptionsModel.tsx b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/useInscriptionsModel.tsx index 05900b0790e9..c3893f978713 100644 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/useInscriptionsModel.tsx +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/useInscriptionsModel.tsx @@ -1,32 +1,36 @@ import { useEffect, useMemo, useState } from "react"; -import { Account } from "@ledgerhq/types-live"; -import { InscriptionsItemProps } from "./index"; -import { mockedItems as InscriptionsMocked } from "LLD/features/Collectibles/__integration__/mockedInscriptions"; +import { SimpleHashNft } from "@ledgerhq/live-nft/api/types"; +import { getInscriptionsData } from "./helpers"; +import { InscriptionsItemProps } from "LLD/features/Collectibles/types/Inscriptions"; + type Props = { - account: Account; + inscriptions: SimpleHashNft[]; }; -export const useInscriptionsModel = ({ account }: Props) => { +export const useInscriptionsModel = ({ inscriptions }: Props) => { const [displayShowMore, setDisplayShowMore] = useState(false); const [displayedObjects, setDisplayedObjects] = useState([]); - const mockedItems: InscriptionsItemProps[] = useMemo(() => [...InscriptionsMocked], []); + const items: InscriptionsItemProps[] = useMemo( + () => getInscriptionsData(inscriptions), + [inscriptions], + ); useEffect(() => { - if (mockedItems.length > 3) setDisplayShowMore(true); - setDisplayedObjects(mockedItems.slice(0, 3)); - }, [mockedItems]); + if (items.length > 3) setDisplayShowMore(true); + setDisplayedObjects(items.slice(0, 3)); + }, [items]); const onShowMore = () => { setDisplayedObjects(prevDisplayedObjects => { const newDisplayedObjects = [ ...prevDisplayedObjects, - ...mockedItems.slice(prevDisplayedObjects.length, prevDisplayedObjects.length + 3), + ...items.slice(prevDisplayedObjects.length, prevDisplayedObjects.length + 3), ]; - if (newDisplayedObjects.length === mockedItems.length) setDisplayShowMore(false); + if (newDisplayedObjects.length === items.length) setDisplayShowMore(false); return newDisplayedObjects; }); }; - return { account, displayedObjects, displayShowMore, onShowMore }; + return { inscriptions: displayedObjects, displayShowMore, onShowMore }; }; diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Loader.tsx b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Loader.tsx new file mode 100644 index 000000000000..476b7cd2abec --- /dev/null +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Loader.tsx @@ -0,0 +1,12 @@ +import { Flex, InfiniteLoader } from "@ledgerhq/react-ui"; +import React from "react"; + +const Loader: React.FC = () => { + return ( + + + + ); +}; + +export default Loader; diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/RareSats/Item.tsx b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/RareSats/Item.tsx index 6845c31536f4..fd763bd2ce32 100644 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/RareSats/Item.tsx +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/RareSats/Item.tsx @@ -2,21 +2,14 @@ import React from "react"; import RowLayout from "LLD/features/Collectibles/Ordinals/components/RareSats/RowLayout"; import IconContainer from "LLD/features/Collectibles/components/Collection/TableRow/IconContainer"; import TokenTitle from "LLD/features/Collectibles/components/Collection/TableRow/TokenTitle"; -import { RareSat } from "LLD/features/Collectibles/types/Ordinals"; import { Text, Flex } from "@ledgerhq/react-ui"; +import { RareSat } from "LLD/features/Collectibles/types/Ordinals"; -const Item = ({ - icons, - name, - year, - count, - utxo_size, - isMultipleRow, -}: RareSat & { isMultipleRow: boolean }) => { +const Item = ({ icons, names, displayed_names, year, count, isMultipleRow }: RareSat) => { const firstColumn = ( - {icons && } - + {icons && } + ); const secondColumn = ( @@ -24,18 +17,12 @@ const Item = ({ {year} ); - const thirdColumn = ( - - {utxo_size} - - ); return ( ); }; diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/RareSats/RowLayout.tsx b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/RareSats/RowLayout.tsx index 31449ce9ec39..a17d3342df9f 100644 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/RareSats/RowLayout.tsx +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/RareSats/RowLayout.tsx @@ -5,7 +5,6 @@ import styled from "styled-components"; type Props = { firstColumnElement: JSX.Element; secondColumnElement: JSX.Element; - thirdColumnElement?: JSX.Element; bgColor?: string; isMultipleRow?: boolean; }; @@ -23,7 +22,6 @@ const Container = styled(Flex)` const RowLayout: React.FC = ({ firstColumnElement, secondColumnElement, - thirdColumnElement, bgColor, isMultipleRow, }) => { @@ -42,9 +40,6 @@ const RowLayout: React.FC = ({ {secondColumnElement} - - {thirdColumnElement} - ); diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/RareSats/TableHeader.tsx b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/RareSats/TableHeader.tsx index 8ce914bf20f9..e38ccead5355 100644 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/RareSats/TableHeader.tsx +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/RareSats/TableHeader.tsx @@ -14,13 +14,11 @@ export const TableHeader = () => { const firstColumn = Column("ordinals.rareSats.table.type"); const secondColumn = Column("ordinals.rareSats.table.year"); - const thirdColumn = Column("ordinals.rareSats.table.utxo"); return ( ); diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/RareSats/helpers.ts b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/RareSats/helpers.ts index 23e7080c2dea..9a7423bbe53b 100644 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/RareSats/helpers.ts +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/RareSats/helpers.ts @@ -1,107 +1,62 @@ import { mappingKeysWithIconAndName } from "../Icons"; -import { MappingKeys } from "LLD/features/Collectibles/types/Ordinals"; -import { IconProps } from "LLD/features/Collectibles/types/Collection"; -import { RareSat } from "LLD/features/Collectibles/types/Ordinals"; +import { SimpleHashNft } from "@ledgerhq/live-nft/api/types"; import { - Satributes, - Subrange, - SatRange, - MockedRareSat, - Sat, - Icons, -} from "LLD/features/Collectibles/types/RareSats"; - -export const processSatType = ( - type: string, - satributes: Satributes, - icons: Icons, - displayNames: string[], - totalCount: number, -) => { - const attribute = satributes[type as MappingKeys]; - if (attribute && attribute.count) { - displayNames.push(type); - if (mappingKeysWithIconAndName[type as MappingKeys]) { - icons[type] = mappingKeysWithIconAndName[type as MappingKeys].icon; + SimpleHashNftWithIcons, + RareSat, + MappingKeys, +} from "LLD/features/Collectibles/types/Ordinals"; + +export function matchCorrespondingIcon(rareSats: SimpleHashNft[]): SimpleHashNftWithIcons[] { + return rareSats.map(rareSat => { + const iconKeys: string[] = []; + if (rareSat.name) { + iconKeys.push(rareSat.name.toLowerCase().replace(" ", "_")); } - totalCount = attribute.count; - } - return { displayNames, totalCount }; -}; - -export const processSatTypes = (satTypes: string[], satributes: Satributes) => { - let displayNames: string[] = []; - let totalCount = 0; - const icons: { [key: string]: ({ size, color, style }: IconProps) => JSX.Element } = {}; - - satTypes.forEach(type => { - const result = processSatType(type, satributes, icons, displayNames, totalCount); - displayNames = result.displayNames; - totalCount = result.totalCount; - }); - - return { displayNames, totalCount, icons }; -}; -export const processSubrange = ( - subrange: Subrange, - satributes: Satributes, - year: string, - value: number, -) => { - const { sat_types } = subrange; - const { displayNames, totalCount, icons } = processSatTypes(sat_types, satributes); - - const name = displayNames - .map(dn => mappingKeysWithIconAndName[dn.toLowerCase().replace(" ", "_") as MappingKeys]?.name) - .filter(Boolean) - .join(" / "); + if (rareSat.extra_metadata?.utxo_details?.satributes) { + iconKeys.push(...Object.keys(rareSat.extra_metadata.utxo_details.satributes)); + } - return { - count: totalCount.toString() + (totalCount === 1 ? " sat" : " sats"), - display_name: displayNames.join(" / "), - year, - utxo_size: value.toString(), - icons, - name, - }; -}; + const icons = iconKeys + .map( + iconKey => + mappingKeysWithIconAndName[iconKey as keyof typeof mappingKeysWithIconAndName]?.icon, + ) + .filter(Boolean) as RareSat["icons"]; -export const processSatRanges = (satRanges: SatRange[], satributes: Satributes) => { - return satRanges.flatMap(range => { - const { year, value, subranges } = range; - return subranges.flatMap(subrange => processSubrange(subrange, satributes, year, value)); + return { ...rareSat, icons }; }); -}; - -export const processRareSat = (sat: Sat) => { - const { extra_metadata } = sat; - const satributes = extra_metadata.utxo_details.satributes as Satributes; - const satRanges = extra_metadata.utxo_details.sat_ranges; - return processSatRanges(satRanges, satributes); -}; - -export const processRareSats = (rareSats: MockedRareSat[]) => { - return rareSats.flatMap(item => item.nfts.flatMap(processRareSat)); -}; - -export const groupRareSats = (processedRareSats: RareSat[]) => { - return processedRareSats.reduce( - (acc, sat) => { - if (!acc[sat.utxo_size]) { - acc[sat.utxo_size] = []; - } - acc[sat.utxo_size].push(sat); - return acc; - }, - {} as Record, - ); -}; +} + +export function createRareSatObject( + rareSats: Record, +): Record { + const result: Record = {}; + + for (const [key, value] of Object.entries(rareSats)) { + result[key] = value.map(rareSat => { + const { icons, extra_metadata } = rareSat; + const year = extra_metadata?.utxo_details?.sat_ranges?.[0]?.year || ""; + const displayed_names = + Object.values(extra_metadata?.utxo_details?.satributes || {}) + .map(attr => attr.display_name) + .join(" / ") || ""; + const names = rareSat.extra_metadata?.utxo_details?.satributes + ? (Object.keys(rareSat.extra_metadata.utxo_details.satributes) as MappingKeys[]) + : []; + const count = extra_metadata?.utxo_details?.value || 0; + const isMultipleRow = value.length > 1; + + return { + year, + displayed_names, + names, + count: `${count} ${count > 1 ? "sats" : "sat"}`, + isMultipleRow, + icons, + }; + }); + } -export const finalizeRareSats = (groupedRareSats: Record) => { - return Object.entries(groupedRareSats).map(([utxo_size, sats]) => ({ - utxo_size, - sats, - isMultipleRow: sats.length > 1, - })); -}; + return result; +} diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/RareSats/index.tsx b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/RareSats/index.tsx index 838d4a0c957f..fe8603eedfb6 100644 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/RareSats/index.tsx +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/RareSats/index.tsx @@ -6,38 +6,64 @@ import Item from "./Item"; import { TableHeaderTitleKey } from "LLD/features/Collectibles/types/Collection"; import { Box, Flex } from "@ledgerhq/react-ui"; import { TableHeader as TableHeaderContainer } from "./TableHeader"; +import { SimpleHashNft } from "@ledgerhq/live-nft/api/types"; +import Loader from "../Loader"; +import Error from "../Error"; -type ViewProps = ReturnType; +type ViewProps = ReturnType & { + isLoading: boolean; + isError: boolean; + isFetched: boolean; +}; + +type Props = { + rareSats: SimpleHashNft[]; + isLoading: boolean; + isError: boolean; + isFetched: boolean; +}; + +function View({ rareSats, isLoading, isError, isFetched }: ViewProps) { + const isLoaded = isFetched; + const hasRareSats = Object.values(rareSats).length > 0; + const dataReady = isLoaded && hasRareSats; -function View({ rareSats }: ViewProps) { return ( - + - + {isLoading && } + {isError && } + {dataReady && } - {rareSats - ? rareSats.map(rareSatGroup => ( - - {rareSatGroup.sats.map(rareSat => ( - - ))} - - )) - : null} - {/** wait for design */} + {dataReady && + Object.entries(rareSats).map(([key, rareSatGroup]) => ( + + {rareSatGroup.map((rareSat, index) => ( + + ))} + + ))} + {isLoaded && !hasRareSats && ( + + {"NOTHING TO SHOW WAITING FOR DESIGN"} + + )} ); } -const RareSats = () => { - return ; +const RareSats = ({ rareSats, isLoading, isError, isFetched }: Props) => { + return ( + + ); }; export default RareSats; diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/RareSats/useRareSatsModel.ts b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/RareSats/useRareSatsModel.ts index 931c590a56d8..a59b1eafc054 100644 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/RareSats/useRareSatsModel.ts +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/RareSats/useRareSatsModel.ts @@ -1,12 +1,14 @@ -import { mockedRareSats } from "LLD/features/Collectibles/__integration__/mockedRareSats"; -import { processRareSats, groupRareSats, finalizeRareSats } from "./helpers"; +import { matchCorrespondingIcon, createRareSatObject } from "./helpers"; +import { SimpleHashNft } from "@ledgerhq/live-nft/api/types"; +import { regroupRareSatsByContractAddress } from "@ledgerhq/live-nft-react"; -type RareSatsProps = {}; - -export const useRareSatsModel = (_props: RareSatsProps) => { - const processedRareSats = processRareSats(mockedRareSats); - const groupedRareSats = groupRareSats(processedRareSats); - const finalRareSats = finalizeRareSats(groupedRareSats); +type Props = { + rareSats: SimpleHashNft[]; +}; - return { rareSats: finalRareSats }; +export const useRareSatsModel = ({ rareSats }: Props) => { + const matchedRareSats = matchCorrespondingIcon(rareSats); + const regroupedRareSats = regroupRareSatsByContractAddress(matchedRareSats); + const rareSatsCreated = createRareSatObject(regroupedRareSats); + return { rareSats: rareSatsCreated }; }; diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/screens/Account/index.tsx b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/screens/Account/index.tsx index 30aed15cccc5..42e34f3e9a61 100644 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/screens/Account/index.tsx +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/screens/Account/index.tsx @@ -1,18 +1,23 @@ import React from "react"; -import { Account } from "@ledgerhq/types-live"; import Inscriptions from "../../components/Inscriptions"; import RareSats from "../../components/RareSats"; import { Flex } from "@ledgerhq/react-ui"; +import useFetchOrdinals from "LLD/features/Collectibles/hooks/useFetchOrdinals"; +import { BitcoinAccount } from "@ledgerhq/coin-bitcoin/lib/types"; type Props = { - account: Account; + account: BitcoinAccount; }; const OrdinalsAccount: React.FC = ({ account }) => { + const { rareSats, inscriptions, ...rest } = useFetchOrdinals({ + account, + }); + return ( - - + + ); }; diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/__integration__/bitcoinPage.test.tsx b/apps/ledger-live-desktop/src/newArch/features/Collectibles/__integration__/bitcoinPage.test.tsx new file mode 100644 index 000000000000..1c836001bcd1 --- /dev/null +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/__integration__/bitcoinPage.test.tsx @@ -0,0 +1,36 @@ +/** + * @jest-environment jsdom + */ +import React from "react"; +import { render, screen, waitFor } from "tests/testUtils"; +import { BitcoinPage } from "./shared"; + +jest.mock( + "electron", + () => ({ ipcRenderer: { on: jest.fn(), send: jest.fn(), invoke: jest.fn() } }), + { virtual: true }, +); + +describe("displayBitcoinPage", () => { + it("should display Bitcoin page with rare sats and inscriptions", async () => { + const { user } = render(, { + initialRoute: `/`, + }); + + await waitFor(() => expect(screen.getByText(/inscription #63691311/i)).toBeVisible()); + await waitFor(() => expect(screen.getByTestId(/raresaticon-palindrome-0/i)).toBeVisible()); + await user.hover(screen.getByTestId(/raresaticon-palindrome-0/i)); + await waitFor(() => expect(screen.getByText(/in a playful twist/i)).toBeVisible()); + await waitFor(() => + expect( + screen.getByText(/block 9 \/ first transaction \/ nakamoto \/ vintage/i), + ).toBeVisible(), + ); + await waitFor(() => expect(screen.getByText(/see more inscriptions/i)).toBeVisible()); + await user.click(screen.getByText(/see more inscriptions/i)); + await waitFor(() => expect(screen.getByText(/timechain #136/i)).toBeVisible()); + await waitFor(() => expect(screen.getByTestId(/raresaticon-jpeg-0/i)).toBeVisible()); + await user.hover(screen.getByTestId(/raresaticon-jpeg-0/i)); + await waitFor(() => expect(screen.getByText(/journey into the past with jpeg/i)).toBeVisible()); + }); +}); diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/__integration__/mockedBTCAccount.ts b/apps/ledger-live-desktop/src/newArch/features/Collectibles/__integration__/mockedBTCAccount.ts new file mode 100644 index 000000000000..a78c34fbbd4d --- /dev/null +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/__integration__/mockedBTCAccount.ts @@ -0,0 +1,54 @@ +import { BitcoinOutput, BitcoinAccount } from "@ledgerhq/coin-bitcoin/lib/types"; +import { CryptoCurrency } from "@ledgerhq/types-cryptoassets"; +import { BalanceHistoryCache } from "@ledgerhq/types-live"; +import BigNumber from "bignumber.js"; + +const utxos: BitcoinOutput[] = [ + { + hash: "abc123def456", + outputIndex: 0, + blockHeight: 700000, + address: "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", + value: new BigNumber(5000000), + rbf: false, + isChange: false, + }, + { + hash: "def456abc123", + outputIndex: 1, + blockHeight: 700001, + address: "1B2zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", + value: new BigNumber(3000000), + rbf: true, + isChange: true, + }, +]; + +export const MockedbtcAccount: BitcoinAccount = { + type: "Account", + id: "mocked-bitcoin-account", + seedIdentifier: "mocked-seed-identifier", + derivationMode: "segwit", + index: 0, + freshAddress: "1C3zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", + freshAddressPath: "44'/0'/0'/0/0", + used: true, + balance: new BigNumber(8000000), + spendableBalance: new BigNumber(8000000), + creationDate: new Date(), + blockHeight: 700001, + currency: {} as CryptoCurrency, + feesCurrency: undefined, + operationsCount: 2, + operations: [], + pendingOperations: [], + lastSyncDate: new Date(), + subAccounts: [], + balanceHistoryCache: {} as BalanceHistoryCache, + swapHistory: [], + syncHash: undefined, + nfts: [], + bitcoinResources: { + utxos, + }, +}; diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/__integration__/mockedInscriptions.ts b/apps/ledger-live-desktop/src/newArch/features/Collectibles/__integration__/mockedInscriptions.ts deleted file mode 100644 index 9d849c33eae6..000000000000 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/__integration__/mockedInscriptions.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { Icons } from "@ledgerhq/react-ui"; -import { InscriptionsItemProps } from "../Ordinals/components/Inscriptions"; - -export const mockedItems: InscriptionsItemProps[] = [ - { - isLoading: false, - tokenName: "NodeMonke#5673", - collectionName: "NodeMonkes", - tokenIcons: [Icons.OrdinalsAlpha, Icons.OrdinalsPaliblockPalindrome], - media: { - uri: "https://www.cryptoslam.io/_next/image?url=https%3A%2F%2Fd6rvidx1ucs57.cloudfront.net%2Fcryptoslam-token-images.s3.amazonaws.com%2FBitcoin%2Fnodemonkes%2Fimage%2Ff976d219206858d782cccd90d25882cc77cafd3d6c159728f0d3407e25961ab0i0.png%3Fd%3D354%26u%3Dhttps%253A%252F%252Fcryptoslam-token-images.s3.amazonaws.com%252FBitcoin%252Fnodemonkes%252Fimage%252Ff976d219206858d782cccd90d25882cc77cafd3d6c159728f0d3407e25961ab0i0.png&w=1920&q=75", - previewUri: - "https://www.cryptoslam.io/_next/image?url=https%3A%2F%2Fd6rvidx1ucs57.cloudfront.net%2Fcryptoslam-token-images.s3.amazonaws.com%2FBitcoin%2Fnodemonkes%2Fimage%2Ff976d219206858d782cccd90d25882cc77cafd3d6c159728f0d3407e25961ab0i0.png%3Fd%3D354%26u%3Dhttps%253A%252F%252Fcryptoslam-token-images.s3.amazonaws.com%252FBitcoin%252Fnodemonkes%252Fimage%252Ff976d219206858d782cccd90d25882cc77cafd3d6c159728f0d3407e25961ab0i0.png&w=1920&q=75", - mediaType: "image", - isLoading: false, - useFallback: true, - contentType: "image", - }, - onClick: () => console.log("clicked"), - }, - { - isLoading: false, - tokenName: "NodeMonke#5673#22", - collectionName: "NodeMonkes#2", - tokenIcons: [Icons.OrdinalsAlpha, Icons.OrdinalsPaliblockPalindrome], - media: { - uri: "https://www.cryptoslam.io/_next/image?url=https%3A%2F%2Fd6rvidx1ucs57.cloudfront.net%2Fcryptoslam-token-images.s3.amazonaws.com%2FBitcoin%2Fnodemonkes%2Fimage%2F33199e5e35a90b516d274e8c076ca205436db00a36bc5a99d2f0e26110ca7abai0.png%3Fd%3D354%26u%3Dhttps%253A%252F%252Fcryptoslam-token-images.s3.amazonaws.com%252FBitcoin%252Fnodemonkes%252Fimage%252F33199e5e35a90b516d274e8c076ca205436db00a36bc5a99d2f0e26110ca7abai0.png&w=1920&q=75", - previewUri: - "https://www.cryptoslam.io/_next/image?url=https%3A%2F%2Fd6rvidx1ucs57.cloudfront.net%2Fcryptoslam-token-images.s3.amazonaws.com%2FBitcoin%2Fnodemonkes%2Fimage%2F33199e5e35a90b516d274e8c076ca205436db00a36bc5a99d2f0e26110ca7abai0.png%3Fd%3D354%26u%3Dhttps%253A%252F%252Fcryptoslam-token-images.s3.amazonaws.com%252FBitcoin%252Fnodemonkes%252Fimage%252F33199e5e35a90b516d274e8c076ca205436db00a36bc5a99d2f0e26110ca7abai0.png&w=1920&q=75", - mediaType: "image", - isLoading: false, - useFallback: true, - contentType: "image", - }, - onClick: () => console.log("clicked"), - }, - { - isLoading: false, - tokenName: "NodeMonke#5673#33", - collectionName: "NodeMonkes#2", - tokenIcons: [], - media: { - uri: "https://www.cryptoslam.io/_next/image?url=https%3A%2F%2Fd6rvidx1ucs57.cloudfront.net%2Fcryptoslam-token-images.s3.amazonaws.com%2FBitcoin%2Fnodemonkes%2Fimage%2F33199e5e35a90b516d274e8c076ca205436db00a36bc5a99d2f0e26110ca7abai0.png%3Fd%3D354%26u%3Dhttps%253A%252F%252Fcryptoslam-token-images.s3.amazonaws.com%252FBitcoin%252Fnodemonkes%252Fimage%252F33199e5e35a90b516d274e8c076ca205436db00a36bc5a99d2f0e26110ca7abai0.png&w=1920&q=75", - previewUri: - "https://www.cryptoslam.io/_next/image?url=https%3A%2F%2Fd6rvidx1ucs57.cloudfront.net%2Fcryptoslam-token-images.s3.amazonaws.com%2FBitcoin%2Fnodemonkes%2Fimage%2F33199e5e35a90b516d274e8c076ca205436db00a36bc5a99d2f0e26110ca7abai0.png%3Fd%3D354%26u%3Dhttps%253A%252F%252Fcryptoslam-token-images.s3.amazonaws.com%252FBitcoin%252Fnodemonkes%252Fimage%252F33199e5e35a90b516d274e8c076ca205436db00a36bc5a99d2f0e26110ca7abai0.png&w=1920&q=75", - mediaType: "image", - isLoading: false, - useFallback: true, - contentType: "image", - }, - onClick: () => console.log("clicked"), - }, - { - isLoading: false, - tokenName: "NodeMonke#5673#44", - collectionName: "NodeMonkes#2", - tokenIcons: [Icons.OrdinalsBlackLegendary], - media: { - uri: "https://www.cryptoslam.io/_next/image?url=https%3A%2F%2Fd6rvidx1ucs57.cloudfront.net%2Fcryptoslam-token-images.s3.amazonaws.com%2FBitcoin%2Fnodemonkes%2Fimage%2F33199e5e35a90b516d274e8c076ca205436db00a36bc5a99d2f0e26110ca7abai0.png%3Fd%3D354%26u%3Dhttps%253A%252F%252Fcryptoslam-token-images.s3.amazonaws.com%252FBitcoin%252Fnodemonkes%252Fimage%252F33199e5e35a90b516d274e8c076ca205436db00a36bc5a99d2f0e26110ca7abai0.png&w=1920&q=75", - previewUri: - "https://www.cryptoslam.io/_next/image?url=https%3A%2F%2Fd6rvidx1ucs57.cloudfront.net%2Fcryptoslam-token-images.s3.amazonaws.com%2FBitcoin%2Fnodemonkes%2Fimage%2F33199e5e35a90b516d274e8c076ca205436db00a36bc5a99d2f0e26110ca7abai0.png%3Fd%3D354%26u%3Dhttps%253A%252F%252Fcryptoslam-token-images.s3.amazonaws.com%252FBitcoin%252Fnodemonkes%252Fimage%252F33199e5e35a90b516d274e8c076ca205436db00a36bc5a99d2f0e26110ca7abai0.png&w=1920&q=75", - mediaType: "image", - isLoading: false, - useFallback: true, - contentType: "image", - }, - onClick: () => console.log("clicked"), - }, - { - isLoading: false, - tokenName: "NodeMonke#5673#55", - collectionName: "NodeMonkes#2", - tokenIcons: [ - Icons.OrdinalsBlock9450X, - Icons.OrdinalsPaliblockPalindrome, - Icons.OrdinalsBlock9, - Icons.OrdinalsHitman, - ], - media: { - uri: "https://www.cryptoslam.io/_next/image?url=https%3A%2F%2Fd6rvidx1ucs57.cloudfront.net%2Fcryptoslam-token-images.s3.amazonaws.com%2FBitcoin%2Fnodemonkes%2Fimage%2F33199e5e35a90b516d274e8c076ca205436db00a36bc5a99d2f0e26110ca7abai0.png%3Fd%3D354%26u%3Dhttps%253A%252F%252Fcryptoslam-token-images.s3.amazonaws.com%252FBitcoin%252Fnodemonkes%252Fimage%252F33199e5e35a90b516d274e8c076ca205436db00a36bc5a99d2f0e26110ca7abai0.png&w=1920&q=75", - previewUri: - "https://www.cryptoslam.io/_next/image?url=https%3A%2F%2Fd6rvidx1ucs57.cloudfront.net%2Fcryptoslam-token-images.s3.amazonaws.com%2FBitcoin%2Fnodemonkes%2Fimage%2F33199e5e35a90b516d274e8c076ca205436db00a36bc5a99d2f0e26110ca7abai0.png%3Fd%3D354%26u%3Dhttps%253A%252F%252Fcryptoslam-token-images.s3.amazonaws.com%252FBitcoin%252Fnodemonkes%252Fimage%252F33199e5e35a90b516d274e8c076ca205436db00a36bc5a99d2f0e26110ca7abai0.png&w=1920&q=75", - mediaType: "image", - isLoading: false, - useFallback: true, - contentType: "image", - }, - onClick: () => console.log("clicked"), - }, -]; diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/__integration__/shared.tsx b/apps/ledger-live-desktop/src/newArch/features/Collectibles/__integration__/shared.tsx index e7faeee601a7..fae8476294d0 100644 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/__integration__/shared.tsx +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/__integration__/shared.tsx @@ -6,6 +6,8 @@ import NFTGallery from "../Nfts/screens/Gallery"; import NftCollection from "../Nfts/screens/Collection"; import NftCollections from "../Nfts/Collections"; import { account } from "./mockedAccount"; +import OrdinalsAccount from "LLD/features/Collectibles/Ordinals/screens/Account"; +import { MockedbtcAccount } from "./mockedBTCAccount"; const NftCollectionNavigation = () => ( @@ -31,3 +33,9 @@ export const NoNftCollectionTest = withRouter(() => ( )); + +export const BitcoinPage = () => ( + + } /> + +); diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/components/Collection/ShowMore.tsx b/apps/ledger-live-desktop/src/newArch/features/Collectibles/components/Collection/ShowMore.tsx index 1e7e844d7da3..52cf18fadd3d 100644 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/components/Collection/ShowMore.tsx +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/components/Collection/ShowMore.tsx @@ -7,15 +7,16 @@ import { useTranslation } from "react-i18next"; type Props = { onShowMore: () => void; + isInscriptions?: boolean; }; -const ShowMore: React.FC = ({ onShowMore }) => { +const ShowMore: React.FC = ({ isInscriptions = false, onShowMore }) => { const { t } = useTranslation(); return ( - {t("NFT.collections.seeMore")} + {isInscriptions ? t("ordinals.inscriptions.seeMore") : t("NFT.collections.seeMore")} diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/components/Collection/TableRow/IconContainer.tsx b/apps/ledger-live-desktop/src/newArch/features/Collectibles/components/Collection/TableRow/IconContainer.tsx deleted file mode 100644 index 79bd9ee94c49..000000000000 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/components/Collection/TableRow/IconContainer.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from "react"; -import { Flex } from "@ledgerhq/react-ui"; -import { IconProps } from "LLD/features/Collectibles/types/Collection"; - -type Props = { - icons: (({ size, color, style }: IconProps) => JSX.Element)[]; -}; - -const IconContainer: React.FC = ({ icons }) => { - return ( - - {icons.map((Icon, index) => ( - - ))} - - ); -}; - -export default IconContainer; diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/components/Collection/TableRow/IconContainer/ToolTip.tsx b/apps/ledger-live-desktop/src/newArch/features/Collectibles/components/Collection/TableRow/IconContainer/ToolTip.tsx new file mode 100644 index 000000000000..6fc73589efba --- /dev/null +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/components/Collection/TableRow/IconContainer/ToolTip.tsx @@ -0,0 +1,19 @@ +import React from "react"; +import Tooltip from "~/renderer/components/Tooltip"; +import { mappingKeysWithIconAndName } from "LLD/features/Collectibles/Ordinals/components/Icons"; +import { useTranslation } from "react-i18next"; +import { MappingKeys } from "LLD/features/Collectibles/types/Ordinals"; + +type ToolTipProps = { + content: MappingKeys; + children: React.ReactNode; +}; + +const RareSatToolTip: React.FC = ({ content, children }) => { + const { t } = useTranslation(); + + const tooltipContent = mappingKeysWithIconAndName[content]?.descriptionTranslationKey || content; + return {children}; +}; + +export default RareSatToolTip; diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/components/Collection/TableRow/IconContainer/index.tsx b/apps/ledger-live-desktop/src/newArch/features/Collectibles/components/Collection/TableRow/IconContainer/index.tsx new file mode 100644 index 000000000000..dadb0a9fe821 --- /dev/null +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/components/Collection/TableRow/IconContainer/index.tsx @@ -0,0 +1,39 @@ +import React from "react"; +import { Flex } from "@ledgerhq/react-ui"; +import { IconProps } from "LLD/features/Collectibles/types/Collection"; +import RareSatToolTip from "./ToolTip"; +import { MappingKeys } from "LLD/features/Collectibles/types/Ordinals"; + +type Props = { + icons: (({ size, color, style }: IconProps) => JSX.Element)[]; + iconNames: MappingKeys[]; +}; + +const IconContainer: React.FC = ({ icons, iconNames }) => { + return ( + + {icons.map( + (Icon, index) => + iconNames && ( + +
+ {/* only for testing nothing displayed since I can't do it on icon */} + + + ), + )} + + ); +}; + +export default IconContainer; diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/components/Collection/TableRow/index.tsx b/apps/ledger-live-desktop/src/newArch/features/Collectibles/components/Collection/TableRow/index.tsx index 6c0ef4bf55d2..bbc2e5c22baf 100644 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/components/Collection/TableRow/index.tsx +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/components/Collection/TableRow/index.tsx @@ -3,11 +3,7 @@ import { Media, Skeleton } from "../../index"; import { Box, Text } from "@ledgerhq/react-ui"; import { rgba } from "~/renderer/styles/helpers"; import styled from "styled-components"; -import { - isNFTRow, - isOrdinalsRow, - isRareSatsRow, -} from "LLD/features/Collectibles/utils/typeGuardsChecker"; +import { isNFTRow, isOrdinalsRow } from "LLD/features/Collectibles/utils/typeGuardsChecker"; import { RowProps as Props } from "LLD/features/Collectibles/types/Collection"; import TokenTitle from "./TokenTitle"; import IconContainer from "./IconContainer"; @@ -28,12 +24,7 @@ const Container = styled(Box)` const TableRow: React.FC = props => { const mediaBox = () => { - return ( - <> - {(isNFTRow(props) || isOrdinalsRow(props)) && } - {isRareSatsRow(props) && null} - - ); + return <>{(isNFTRow(props) || isOrdinalsRow(props)) && }; }; const nftCount = () => { @@ -47,7 +38,7 @@ const TableRow: React.FC = props => { )} {isOrdinalsRow(props) && props.tokenIcons.length != 0 && ( - + )} ); diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/hooks/useFetchOrdinals.ts b/apps/ledger-live-desktop/src/newArch/features/Collectibles/hooks/useFetchOrdinals.ts new file mode 100644 index 000000000000..01da9b38977c --- /dev/null +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/hooks/useFetchOrdinals.ts @@ -0,0 +1,18 @@ +import { useFetchOrdinals as fetchOrdinalsFromSimpleHash } from "@ledgerhq/live-nft-react"; +import { BitcoinAccount } from "@ledgerhq/coin-bitcoin/lib/types"; + +type Props = { + account: BitcoinAccount; +}; + +const useFetchOrdinals = ({ account }: Props) => { + const utxosAddresses = account.bitcoinResources?.utxos?.map(utxo => utxo.address).join(",") || ""; + const { rareSats, inscriptions, isLoading, isError, isFetched } = fetchOrdinalsFromSimpleHash({ + addresses: utxosAddresses, + threshold: 0, + }); + + return { rareSats, inscriptions, isLoading, isError, isFetched }; +}; + +export default useFetchOrdinals; diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/hooks/useNftLinks.tsx b/apps/ledger-live-desktop/src/newArch/features/Collectibles/hooks/useNftLinks.tsx index b33df419af6d..489f48cbf59d 100644 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/hooks/useNftLinks.tsx +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/hooks/useNftLinks.tsx @@ -15,7 +15,7 @@ import CustomImage from "~/renderer/screens/customImage"; import { ContextMenuItemType } from "~/renderer/components/ContextMenu/ContextMenuWrapper"; import { devicesModelListSelector } from "~/renderer/reducers/settings"; import { safeList } from "LLD/features/Collectibles/utils/useSafeList"; -import { ItemType } from "LLD/features/Collectibles/types/Links"; +import { ItemType } from "~/newArch/features/Collectibles/types/enum/Links"; const linksPerCurrency: Record< string, diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/types/Collectibles.ts b/apps/ledger-live-desktop/src/newArch/features/Collectibles/types/Collectibles.ts index aa557daae387..140e179b2da2 100644 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/types/Collectibles.ts +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/types/Collectibles.ts @@ -1,9 +1,15 @@ import { ProtoNFT, NFT, Account } from "@ledgerhq/types-live"; import { CollectibleTypeEnum } from "./enum/Collectibles"; -export type CollectibleType = CollectibleTypeEnum.NFT | CollectibleTypeEnum.Ordinal; +export type CollectibleType = + | CollectibleTypeEnum.NFT + | CollectibleTypeEnum.Ordinal + | CollectibleTypeEnum.RareSat + | CollectibleTypeEnum.Inscriptions; export type BaseNftsProps = { nfts: (ProtoNFT | NFT)[]; account: Account; }; + +export type Status = "error" | "success" | "pending"; diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/types/Collection.ts b/apps/ledger-live-desktop/src/newArch/features/Collectibles/types/Collection.ts index c2fd9cf1e8bf..743e4cd4637f 100644 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/types/Collection.ts +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/types/Collection.ts @@ -1,4 +1,5 @@ import { MediaProps } from "./Media"; +import { MappingKeys } from "./Ordinals"; export type NftRowProps = { media: MediaProps; @@ -18,6 +19,7 @@ export type OrdinalsRowProps = { tokenName: string; collectionName: string; tokenIcons: Array<({ size, color, style }: IconProps) => JSX.Element>; + rareSatName: MappingKeys[]; onClick: () => void; }; diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/types/Inscriptions.ts b/apps/ledger-live-desktop/src/newArch/features/Collectibles/types/Inscriptions.ts new file mode 100644 index 000000000000..ed2fd9f9088a --- /dev/null +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/types/Inscriptions.ts @@ -0,0 +1,12 @@ +import { OrdinalsRowProps } from "./Collection"; +import { MediaProps } from "./Media"; +import { MappingKeys } from "./Ordinals"; + +export type InscriptionsItemProps = { + tokenName: string; + collectionName: string; + tokenIcons: OrdinalsRowProps["tokenIcons"]; + media: MediaProps; + rareSatName?: MappingKeys[]; + onClick: () => void; +}; diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/types/Links.ts b/apps/ledger-live-desktop/src/newArch/features/Collectibles/types/Links.ts deleted file mode 100644 index a8e116faa2b5..000000000000 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/types/Links.ts +++ /dev/null @@ -1,3 +0,0 @@ -export enum ItemType { - EXTERNAL = "external", -} diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/types/Ordinals.ts b/apps/ledger-live-desktop/src/newArch/features/Collectibles/types/Ordinals.ts index 38ce93b53a2a..2902f8a4bdae 100644 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/types/Ordinals.ts +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/types/Ordinals.ts @@ -1,14 +1,19 @@ +import { SimpleHashNft } from "@ledgerhq/live-nft/api/types"; import { mappingKeysWithIconAndName } from "../Ordinals/components/Icons"; import { IconProps } from "./Collection"; export type MappingKeys = keyof typeof mappingKeysWithIconAndName; -export type RareSat = { - count: string; - display_name: string | string[]; +type Icon = ({ size, color, style }: IconProps) => JSX.Element; + +export interface RareSat { + displayed_names: string; + icons?: Icon[]; year: string; - utxo_size: string; - icons?: { [key: string]: ({ size, color, style }: IconProps) => JSX.Element }; - name: string; - isDoubleRow?: boolean; -}; + count: string; + names: MappingKeys[]; + isMultipleRow: boolean; +} +export interface SimpleHashNftWithIcons extends SimpleHashNft { + icons?: Icon[]; +} diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/types/RareSats.ts b/apps/ledger-live-desktop/src/newArch/features/Collectibles/types/RareSats.ts index 0aa47984190e..ad620f3b5af6 100644 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/types/RareSats.ts +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/types/RareSats.ts @@ -52,11 +52,3 @@ export type ExtraMetadata = { animation_original_url: string | null; metadata_original_url: string | null; }; - -export type Sat = { - extra_metadata: ExtraMetadata; -}; - -export type MockedRareSat = { - nfts: Sat[]; -}; diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/types/enum/Collectibles.ts b/apps/ledger-live-desktop/src/newArch/features/Collectibles/types/enum/Collectibles.ts index a4c730ef9630..db234075d289 100644 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/types/enum/Collectibles.ts +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/types/enum/Collectibles.ts @@ -1,4 +1,12 @@ export enum CollectibleTypeEnum { NFT = "NFT", Ordinal = "Ordinal", + RareSat = "RareSat", + Inscriptions = "Inscriptions", +} + +export enum TanStackStatus { + Error = "error", + Success = "success", + Pending = "pending", } diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/types/enum/DetailDrawer.ts b/apps/ledger-live-desktop/src/newArch/features/Collectibles/types/enum/DetailDrawer.ts index da05e93321d9..e5bfbe7fbd4b 100644 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/types/enum/DetailDrawer.ts +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/types/enum/DetailDrawer.ts @@ -5,6 +5,7 @@ export enum FieldStatus { NoData = "nodata", Queued = "queued", } + export enum ItemType { Separator = "separator", ExternalLink = "external", diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/utils/typeGuardsChecker.ts b/apps/ledger-live-desktop/src/newArch/features/Collectibles/utils/typeGuardsChecker.ts index d471caa3806e..1f6168d989ed 100644 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/utils/typeGuardsChecker.ts +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/utils/typeGuardsChecker.ts @@ -1,18 +1,9 @@ -import { - NftRowProps, - OrdinalsRowProps, - RareSatsRowProps, - RowProps as Props, -} from "../types/Collection"; +import { NftRowProps, OrdinalsRowProps, RowProps as Props } from "../types/Collection"; export function isNFTRow(props: Props): props is Props & NftRowProps { return "media" in props && !("collectionName" in props); } export function isOrdinalsRow(props: Props): props is Props & OrdinalsRowProps { - return "collectionName" in props; -} - -export function isRareSatsRow(props: Props): props is Props & RareSatsRowProps { - return "tokenIcons" in props; + return "rareSatName" in props; } diff --git a/apps/ledger-live-desktop/src/renderer/screens/account/index.tsx b/apps/ledger-live-desktop/src/renderer/screens/account/index.tsx index 26c74695c8c6..f420cd4fe35b 100644 --- a/apps/ledger-live-desktop/src/renderer/screens/account/index.tsx +++ b/apps/ledger-live-desktop/src/renderer/screens/account/index.tsx @@ -44,6 +44,7 @@ import { useLocalizedUrl } from "~/renderer/hooks/useLocalizedUrls"; import { urls } from "~/config/urls"; import { CurrencyConfig } from "@ledgerhq/coin-framework/config"; import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; +import { isBitcoinBasedAccount, isBitcoinAccount } from "@ledgerhq/live-common/account/typeGuards"; type Params = { id: string; @@ -146,6 +147,9 @@ const AccountPage = ({ const color = getCurrencyColor(currency, bgColor); + const displayOrdinals = + isOrdinalsEnabled && isBitcoinBasedAccount(account) && isBitcoinAccount(account); + return ( ) ) : null} - {isOrdinalsEnabled && account.type === "Account" && account.currency.id === "bitcoin" ? ( - - ) : null} + {displayOrdinals ? : null} {account.type === "Account" ? : null} { - return HttpResponse.json(mockedResponse.tokenInfos1); + return HttpResponse.json(ETHmockedResponse.tokenInfos1); }), http.get("https://simplehash.api.live.ledger.com/api/v0/nfts/owners_v2", ({ request }) => { const url = new URL(request.url); @@ -13,7 +14,11 @@ const handlers = [ const limit = url.searchParams.get("limit"); const filters = url.searchParams.get("filters"); - return HttpResponse.json(mockedResponse.simplehash); + if (chains === "bitcoin,utxo" && walletAddresses) + return HttpResponse.json(OrdinalsMockedResponse.mockedResponse1); + + if (chains === "ethereum" && walletAddresses) + return HttpResponse.json(ETHmockedResponse.simplehash); }), ]; diff --git a/libs/ledger-live-common/.unimportedrc.json b/libs/ledger-live-common/.unimportedrc.json index 5691d61c148e..93928437fda7 100644 --- a/libs/ledger-live-common/.unimportedrc.json +++ b/libs/ledger-live-common/.unimportedrc.json @@ -329,7 +329,8 @@ "src/walletSync/getEnvironmentParams.ts", "src/device/use-cases/appDataBackup/deleteAppData.ts", "src/device/use-cases/appDataBackup/deleteAppDataUseCase.ts", - "src/device/use-cases/appDataBackup/deleteAppDataUseCaseDI.ts" + "src/device/use-cases/appDataBackup/deleteAppDataUseCaseDI.ts", + "src/account/typeGuards.ts" ], "ignoreUnused": [ "@ledgerhq/hw-transport-mocker", diff --git a/libs/ledger-live-common/src/account/typeGuards.ts b/libs/ledger-live-common/src/account/typeGuards.ts new file mode 100644 index 000000000000..c4b3dce773af --- /dev/null +++ b/libs/ledger-live-common/src/account/typeGuards.ts @@ -0,0 +1,10 @@ +import { BitcoinAccount } from "@ledgerhq/coin-bitcoin/lib/types"; +import { Account, AccountLike } from "@ledgerhq/types-live"; + +export function isBitcoinBasedAccount(account: Account | AccountLike): account is BitcoinAccount { + return (account as BitcoinAccount).bitcoinResources !== undefined; +} + +export function isBitcoinAccount(account: Account): boolean { + return account.currency.id === "bitcoin"; +} diff --git a/libs/live-nft-react/src/hooks/__tests__/useFetchOrdinals.test.tsx b/libs/live-nft-react/src/hooks/__tests__/useFetchOrdinals.test.tsx new file mode 100644 index 000000000000..0fcd7ead9991 --- /dev/null +++ b/libs/live-nft-react/src/hooks/__tests__/useFetchOrdinals.test.tsx @@ -0,0 +1,42 @@ +import { useFetchOrdinals } from "../useFetchOrdinals"; +import { renderHook } from "@testing-library/react"; +import { wrapper } from "../../tools/helperTests"; +import { useInfiniteQuery } from "@tanstack/react-query"; + +jest.mock("@tanstack/react-query", () => ({ + ...jest.requireActual("@tanstack/react-query"), + useInfiniteQuery: jest.fn(), +})); + +const mockedBTCAddresses = "bc1pgtat0n2kavrz4ufhngm2muzxzx6pcmvr4czp089v48u5sgvpd9vqjsuaql"; +const threshold = 0.5; + +const mockQueryResult = { + data: { + pages: [{ nfts: [] }], + }, + isLoading: false, + isError: false, + fetchNextPage: jest.fn(), + hasNextPage: false, +}; + +describe("useFetchOrdinals", () => { + it("calls useInfiniteQuery with correct arguments", async () => { + (useInfiniteQuery as jest.Mock).mockReturnValue(mockQueryResult); + + renderHook(() => useFetchOrdinals({ addresses: mockedBTCAddresses, threshold }), { wrapper }); + + expect(useInfiniteQuery).toHaveBeenCalledWith({ + queryKey: [ + "FetchOrdinals", + "bc1pgtat0n2kavrz4ufhngm2muzxzx6pcmvr4czp089v48u5sgvpd9vqjsuaql", + ["bitcoin", "utxo"], + ], + queryFn: expect.any(Function), + initialPageParam: undefined, + getNextPageParam: expect.any(Function), + enabled: true, + }); + }); +}); diff --git a/libs/live-nft-react/src/hooks/helpers/ordinals.ts b/libs/live-nft-react/src/hooks/helpers/ordinals.ts new file mode 100644 index 000000000000..3d04373c8045 --- /dev/null +++ b/libs/live-nft-react/src/hooks/helpers/ordinals.ts @@ -0,0 +1,91 @@ +import { SimpleHashNft } from "@ledgerhq/live-nft/api/types"; +import { OrdinalsChainsEnum } from "../types"; + +/** + * Categorizes an array of NFTs into two categories: rareSats and inscriptions. + * @param nfts - The array of NFTs to categorize. + * @returns An object containing two arrays: rareSats and inscriptions. + */ +export function categorizeNftsByChain(nfts: SimpleHashNft[]): { + rareSats: SimpleHashNft[]; + inscriptions: SimpleHashNft[]; +} { + const initialAccumulator = { + rareSats: [] as SimpleHashNft[], + inscriptions: [] as SimpleHashNft[], + }; + + return nfts.reduce((accumulator, nft) => { + if (nft.chain === OrdinalsChainsEnum.INSCRIPTIONS) accumulator.inscriptions.push(nft); + else accumulator.rareSats.push(nft); + return accumulator; + }, initialAccumulator); +} + +/** + * Processes an array of rareSats by removing the common rarity. + * it's easily changeable to remove other rarity + * @param rareSats - An array of rareSats to process. + * @returns An array of rareSats with the common rarity removed. + */ +export function removeCommonRareSats(rareSats: SimpleHashNft[]): SimpleHashNft[] { + return rareSats + .map(nft => { + const utxoDetails = nft.extra_metadata?.utxo_details; + if (utxoDetails) { + const { common, ...restSatributes } = utxoDetails.satributes; + return { + ...nft, + extra_metadata: { + ...nft.extra_metadata, + utxo_details: { + ...utxoDetails, + satributes: restSatributes, + }, + }, + }; + } + return nft; + }) + .filter(nft => { + const utxoDetails = nft.extra_metadata?.utxo_details; + if (utxoDetails) { + const keys = Object.keys(utxoDetails.satributes); + return keys.length > 0; + } + return true; + }); +} +/** + * Processes an array of rareSats by regrouping them by Contract Address. + * @param rareSats - An array of rareSats to process. + * @returns An array of rareSats with the common rarity removed. + */ +export function regroupRareSatsByContractAddress( + rareSats: SimpleHashNft[], +): Record { + return rareSats.reduce>((acc, sat) => { + const { contract_address } = sat; + acc[contract_address] = acc[contract_address] || []; + acc[contract_address].push(sat); + return acc; + }, {}); +} + +/** + * Processes the NFTs by restructuring them. + * @param nfts - The array of NFTs to process. + * @returns An object containing two arrays: rareSats and inscriptions. + */ +export function processOrdinals(nfts: SimpleHashNft[]): { + rareSats: SimpleHashNft[]; + inscriptions: SimpleHashNft[]; +} { + const { rareSats, inscriptions } = categorizeNftsByChain(nfts); + const rareSatsWithoutCommonSats = removeCommonRareSats(rareSats); + + return { + rareSats: rareSatsWithoutCommonSats, + inscriptions, + }; +} diff --git a/libs/live-nft-react/src/hooks/types.ts b/libs/live-nft-react/src/hooks/types.ts index efa0467d75ed..7321a50548be 100644 --- a/libs/live-nft-react/src/hooks/types.ts +++ b/libs/live-nft-react/src/hooks/types.ts @@ -50,3 +50,52 @@ export type RefreshMetadataResult = UseMutationResult< // Check Spam Score Contract or NFT export type CheckSpamScoreResult = UseQueryResult; + +// Fetch Ordinals from SimpleHash +export enum OrdinalsChainsEnum { + RARESATS = "utxo", + INSCRIPTIONS = "bitcoin", +} +export type OrdinalsStandard = "raresats" | "inscriptions"; +export type FetchNftsProps = { + addresses: string; + threshold: number; +}; +export enum RareSatRarity { + ALPHA = "alpha", + BLACK_EPIC = "black_epic", + BLACK_LEGENDARY = "black_legendary", + BLACK_MYTHIC = "black_mythic", + BLACK_RARE = "black_rare", + BLACK_UNCOMMON = "black_uncommon", + BLOCK_9 = "block_9", + BLOCK_9_450X = "block_9_450x", + BLOCK_78 = "block_78", + BLOCK_286 = "block_286", + BLOCK_666 = "block_666", + COMMON = "common", + EPIC = "epic", + FIRST_TX = "first_tx", + HITMAN = "hitman", + JPEG = "jpeg", + LEGACY = "legacy", + LEGENDARY = "legendary", + MYTHIC = "mythic", + NAKAMOTO = "nakamoto", + OMEGA = "omega", + PALIBLOCK = "paliblock", + PALINDROME = "palindrome", + PALINCEPTION = "palinception", + PIZZA = "pizza", + RARE = "rare", + UNCOMMON = "uncommon", + VINTAGE = "vintage", + LOW_SERIAL_NUMBER = "low_serial_number", + SPECIAL_TRANSACTION = "special_transaction", + COINBASE_REWARD = "coinbase_reward", + DUST = "dust", + UNIQUE_PATTERN = "unique_pattern", + COLORED_COIN = "colored_coin", + HISTORICAL_EVENT = "historical_event", + NON_STANDARD_SCRIPT = "non_standard_script", +} diff --git a/libs/live-nft-react/src/hooks/useFetchOrdinals.ts b/libs/live-nft-react/src/hooks/useFetchOrdinals.ts new file mode 100644 index 000000000000..0464912acbba --- /dev/null +++ b/libs/live-nft-react/src/hooks/useFetchOrdinals.ts @@ -0,0 +1,49 @@ +import { useEffect, useMemo } from "react"; +import { fetchNftsFromSimpleHash } from "@ledgerhq/live-nft/api/simplehash"; +import { InfiniteData, useInfiniteQuery, UseInfiniteQueryResult } from "@tanstack/react-query"; +import { SimpleHashResponse, SimpleHashNft } from "@ledgerhq/live-nft/api/types"; +import { FetchNftsProps, OrdinalsChainsEnum } from "./types"; +import { NFTS_QUERY_KEY } from "../queryKeys"; +import { processOrdinals } from "./helpers/ordinals"; + +type Result = UseInfiniteQueryResult, Error> & { + rareSats: SimpleHashNft[]; + inscriptions: SimpleHashNft[]; +}; + +export function useFetchOrdinals({ addresses, threshold }: FetchNftsProps): Result { + const chains = [OrdinalsChainsEnum.INSCRIPTIONS, OrdinalsChainsEnum.RARESATS]; + const addressString = Array.isArray(addresses) ? addresses.join(",") : addresses; + const queryResult = useInfiniteQuery({ + queryKey: [NFTS_QUERY_KEY.FetchOrdinals, addresses, chains], + queryFn: ({ pageParam }: { pageParam: string | undefined }) => + fetchNftsFromSimpleHash({ + addresses: addressString, + chains, + cursor: pageParam, + threshold, + }), + initialPageParam: undefined, + getNextPageParam: lastPage => lastPage.next_cursor, + enabled: addresses.length > 0, + }); + + const nfts = useMemo( + () => queryResult.data?.pages.flatMap(page => page.nfts) ?? [], + [queryResult.data], + ); + + const { rareSats, inscriptions } = useMemo(() => processOrdinals(nfts), [nfts]); + + useEffect(() => { + if (queryResult.hasNextPage && !queryResult.isFetchingNextPage) { + queryResult.fetchNextPage(); + } + }, [queryResult, queryResult.hasNextPage, queryResult.isFetchingNextPage]); + + return { + ...queryResult, + rareSats, + inscriptions, + }; +} diff --git a/libs/live-nft-react/src/index.ts b/libs/live-nft-react/src/index.ts index 3d8c50c23a88..db82f03c1f90 100644 --- a/libs/live-nft-react/src/index.ts +++ b/libs/live-nft-react/src/index.ts @@ -4,3 +4,5 @@ export * from "./hooks/useSpamReportNft"; export * from "./hooks/useNftFloorPrice"; export * from "./hooks/useRefreshMetadata"; export * from "./hooks/useCheckSpamScore"; +export * from "./hooks/useFetchOrdinals"; +export * from "./hooks/helpers/ordinals"; diff --git a/libs/live-nft-react/src/queryKeys.ts b/libs/live-nft-react/src/queryKeys.ts index 9405a157801a..78730a322ce0 100644 --- a/libs/live-nft-react/src/queryKeys.ts +++ b/libs/live-nft-react/src/queryKeys.ts @@ -3,4 +3,5 @@ export const NFTS_QUERY_KEY = { SpamReport: "SpamReport", FloorPrice: "FloorPrice", CheckSpamScore: "CheckSpamScore", + FetchOrdinals: "FetchOrdinals", }; diff --git a/libs/live-nft/src/api/types.ts b/libs/live-nft/src/api/types.ts index e2804a382464..4491f38c75da 100644 --- a/libs/live-nft/src/api/types.ts +++ b/libs/live-nft/src/api/types.ts @@ -11,6 +11,63 @@ export interface SimpleHashRefreshResponse { readonly message: string; } +export interface UtxoDetails { + readonly distinct_rare_sats: number; + readonly satributes: { + readonly [key: string]: { + readonly count: number; + readonly display_name: string; + readonly description: string; + readonly icon: string; + }; + }; + readonly sat_ranges: { + readonly starting_sat: number; + readonly value: number; + readonly distinct_rare_sats: number; + readonly year: string; + readonly subranges: { + readonly starting_sat: number; + readonly value: number; + readonly sat_types: string[]; + }[]; + }[]; + readonly block_number: number; + readonly value: number; + readonly script_pub_key: { + readonly asm: string; + readonly desc: string; + readonly hex: string; + readonly address: string; + readonly type: string; + }; +} + +export interface ordinal_details { + readonly charms: string | null; + readonly content_length: number; + readonly content_type: string; + readonly inscription_id: string; + readonly inscription_number: number; + readonly location: string; + readonly output_value: number; + readonly parents: string | null; + readonly protocol_content: string | null; + readonly protocol_name: string | null; + readonly sat_name: string; + readonly sat_number: number; + readonly sat_rarity: string; +} + +export interface preview { + readonly blurhash: string; + readonly image_large_url: string; + readonly image_medium_url: string; + readonly image_opengraph_url: string; + readonly image_small_url: string; + readonly predominate_color: string; +} + export interface SimpleHashNft { readonly nft_id: string; readonly chain: string; @@ -20,17 +77,22 @@ export interface SimpleHashNft { readonly name: string; readonly description: string; readonly token_count: number; + readonly previews?: preview; + readonly other_url?: string; readonly collection: { readonly name: string; readonly spam_score: number; }; readonly contract: { readonly type: string; + readonly name?: string; }; readonly extra_metadata?: { readonly ledger_metadata?: { readonly ledger_stax_image: string; }; + readonly utxo_details?: UtxoDetails; + readonly ordinal_details?: ordinal_details; readonly image_original_url: string; readonly animation_original_url: string; };