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..ac88d55bbbd3 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, isLoading, isError, isFetched } = 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..9b0882f3b1b2 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 { BitcoinAccount } from "@ledgerhq/coin-bitcoin/lib/types"; type Params = { id: string; @@ -146,6 +147,14 @@ const AccountPage = ({ const color = getCurrencyColor(currency, bgColor); + function isBitcoinBasedAccount(account: Account | AccountLike): account is BitcoinAccount { + return (account as BitcoinAccount).bitcoinResources !== undefined; + } + + function isBitcoinAccount(account: BitcoinAccount): boolean { + return account.currency.id === "bitcoin"; + } + return ( ) ) : null} - {isOrdinalsEnabled && account.type === "Account" && account.currency.id === "bitcoin" ? ( + {isOrdinalsEnabled && isBitcoinBasedAccount(account) && isBitcoinAccount(account) ? ( ) : null} {account.type === "Account" ? : null} diff --git a/apps/ledger-live-desktop/static/i18n/en/app.json b/apps/ledger-live-desktop/static/i18n/en/app.json index b50be12dafa5..8aabb1889dd4 100644 --- a/apps/ledger-live-desktop/static/i18n/en/app.json +++ b/apps/ledger-live-desktop/static/i18n/en/app.json @@ -6595,12 +6595,45 @@ } }, "ordinals": { + "inscriptions": { "seeMore": "See more inscriptions" }, "rareSats": { "title": "Rare Sats", "table": { "type": "Sat Type / Amount", "year": "Year", "utxo": "UTXO size" + }, + "rareSat": { + "description": { + "alpha": "Step into the birth of each bitcoin with Alpha, marking the initial satoshi. This label signifies the genesis of a Bitcoin unit.", + "black_epic": "The concluding satoshi of each halving event, which occurs approximately every four years when the Bitcoin mining reward is halved.", + "black_legendary": "No Description for Black Legendary", + "black_mythic": "No Description for Black Mythic", + "black_rare": "Denotes the final satoshi at the conclusion of each mining difficulty adjustment period.", + "black_uncommon": "The concluding satoshi of each halving event, which occurs approximately every four years when the Bitcoin mining reward is halved.", + "block_9": "Block 9 holds a unique distinction in Bitcoin's history. As one of the earliest blocks mined, the satoshis contained within it are some of the oldest sats in circulation. These sats offer a tangible connection to the very beginnings of Bitcoin, shortly after its inception.", + "block_9_450x": "Step back in time with Block 9 450x, where sats in first bitcoin of the 9th block are highlighted. This label unfolds a captivating chapter in Bitcoin's history.", + "block_78": "Block 78 holds a profound place in the annals of Bitcoin's history. This block was mined by Hal Finney, marking the first instance where someone other than the elusive Satoshi Nakamoto contributed to the blockchain's growth.", + "block_286": "Unearth the significance of Block 286, representing satoshis from the second-ever Bitcoin transaction mined by Satoshi Nakamoto. These sats carry the weight of Bitcoin's elusive creator and contribute to the narrative of its enigmatic origins.", + "block_666": "No Description for Block 666", + "common": "No Description for Common", + "epic": "Every four years, the Bitcoin network experiences a \"halving\" event where the mining reward is cut in half. The first sat of each of these halving epochs is labeled \"Epic,\" marking a significant moment in Bitcoin's economic model.", + "first_tx": "On January 12, 2009, history was made when Satoshi Nakamoto sent 10 Bitcoins to a computer scientist named Hal Finney. This marked the first-ever Bitcoin transaction. Sats originating from this iconic transaction bear the \"First Transaction\" label, symbolizing the genesis of Bitcoin's peer-to-peer exchange.", + "hitman": "No Description for Hitman", + "jpeg": "Journey into the past with JPEG, as these satoshis might be tied to the first Bitcoin trade for an image on February 24, 2010. This label narrates a tale of innovation, showcasing Bitcoin's early forays into unconventional transactions and exchanges.", + "legacy": "No Description for Legacy", + "legendary": "No Description for Legendary", + "mythic": "No Description for Mythic", + "nakamoto": "The mystique surrounding Bitcoin's pseudonymous creator, Satoshi Nakamoto, remains undiminished. Sats that were mined by Nakamoto himself are branded with this label, making them highly sought after by collectors.", + "omega": "Experience the poetic conclusion of every bitcoin with Omega, the last satoshi. As the final satoshi in a Bitcoin unit.", + "paliblock": "No Description for PaliBlock Palindrome", + "palindrome": "In a playful twist, any sat whose number can be read the same forwards and backwards is branded a \"Palindrome.\" For example, a sat numbered 12321 or 45654. These unique numerical attributes add a layer of rarity and curiosity to the labeled sats.", + "palinception": "Delve into the mesmerizing world of uniform palinception, where each satoshi forms a palindrome of palindromes. These intricately structured sats create a fascinating tapestry of mirrored digits.", + "pizza": "One of the most iconic stories in the Bitcoin community is that of a programmer who paid 10,000 Bitcoins for two Papa John's pizzas on May 22, 2010. This event is often cited to demonstrate the meteoric rise in Bitcoin's value. Sats from this legendary transaction wear the \"Pizza\" label.", + "rare": "The Bitcoin network periodically adjusts its mining difficulty to ensure that blocks are added roughly every ten minutes. The first sat of each such adjustment period is branded as \"Rare.\" This reflects the ever-evolving nature of the network's security and consensus mechanisms. ", + "uncommon": "This label is designated to the first satoshi of every block. Given that a new block is added to the Bitcoin blockchain approximately every 10 minutes, these sats capture the very beginning of that timeframe.", + "vintage": "The dawn of Bitcoin was marked by its initial 1,000 blocks. Sats mined within this period are branded \"Vintage,\"." + } } } } diff --git a/apps/ledger-live-desktop/tests/handlers/fixtures/nfts/index.ts b/apps/ledger-live-desktop/tests/handlers/fixtures/nfts/index.ts index 28e912f84400..b8af401b80df 100644 --- a/apps/ledger-live-desktop/tests/handlers/fixtures/nfts/index.ts +++ b/apps/ledger-live-desktop/tests/handlers/fixtures/nfts/index.ts @@ -1,7 +1,7 @@ import mockedResponse1 from "./mockedResponse1.json"; import mockedResponse2 from "./mockedResponse2.json"; -export const mockedResponse = { +export const ETHmockedResponse = { tokenInfos1: mockedResponse1, simplehash: mockedResponse2, }; diff --git a/apps/ledger-live-desktop/tests/handlers/fixtures/ordinals/index.ts b/apps/ledger-live-desktop/tests/handlers/fixtures/ordinals/index.ts new file mode 100644 index 000000000000..b904276b4d16 --- /dev/null +++ b/apps/ledger-live-desktop/tests/handlers/fixtures/ordinals/index.ts @@ -0,0 +1,5 @@ +import mockedResponse1 from "./mockedOrdinals.json"; + +export const OrdinalsMockedResponse = { + mockedResponse1: mockedResponse1, +}; diff --git a/apps/ledger-live-desktop/tests/handlers/fixtures/ordinals/mockedOrdinals.json b/apps/ledger-live-desktop/tests/handlers/fixtures/ordinals/mockedOrdinals.json new file mode 100644 index 000000000000..09b50da03fe3 --- /dev/null +++ b/apps/ledger-live-desktop/tests/handlers/fixtures/ordinals/mockedOrdinals.json @@ -0,0 +1,2575 @@ +{ + "next_cursor": null, + "next": null, + "previous": null, + "nfts": [ + { + "nft_id": "utxo.7893390614d9a5608e39ea637e5794f7564d3fcdd5ad46105f349c2cf45272fb.1", + "chain": "utxo", + "contract_address": "7893390614d9a5608e39ea637e5794f7564d3fcdd5ad46105f349c2cf45272fb", + "token_id": "1", + "name": null, + "description": null, + "previews": { + "image_small_url": null, + "image_medium_url": null, + "image_large_url": null, + "image_opengraph_url": null, + "blurhash": null, + "predominant_color": null + }, + "image_url": null, + "image_properties": null, + "video_url": null, + "video_properties": null, + "audio_url": null, + "audio_properties": null, + "model_url": null, + "model_properties": null, + "other_url": null, + "other_properties": null, + "background_color": null, + "external_url": null, + "created_date": "2024-03-14T11:34:34", + "status": "minted", + "token_count": 1, + "owner_count": 1, + "owners": [ + { + "owerner_address": "weneverknow", + "quantity": 1, + "quantity_string": "1", + "first_acquired_date": "2024-03-14T11:34:34", + "last_acquired_date": "2024-03-14T11:34:34" + } + ], + "contract": { + "type": "UTXO", + "name": null, + "symbol": null, + "deployed_by": null, + "deployed_via_contract": null, + "owned_by": null, + "has_multiple_collections": false + }, + "collection": { + "collection_id": null, + "name": null, + "description": null, + "image_url": null, + "image_properties": null, + "banner_image_url": null, + "category": null, + "is_nsfw": null, + "external_url": null, + "twitter_username": null, + "discord_url": null, + "instagram_username": null, + "medium_username": null, + "telegram_url": null, + "marketplace_pages": [], + "metaplex_mint": null, + "metaplex_candy_machine": null, + "metaplex_first_verified_creator": null, + "spam_score": null, + "floor_prices": [], + "top_bids": [], + "distinct_owner_count": null, + "distinct_nft_count": null, + "total_quantity": null, + "chains": [], + "top_contracts": [], + "collection_royalties": [] + }, + "last_sale": null, + "primary_sale": null, + "first_created": { + "minted_to": "bc1pgtat0n2kavrz4ufhngm2muzxzx6pcmvr4czp089v48u5sgvpd9vqjsuaql", + "quantity": 1, + "quantity_string": "1", + "timestamp": "2024-03-14T11:34:34", + "block_number": 834655, + "transaction": "", + "transaction_initiator": null + }, + "rarity": { + "rank": null, + "score": null, + "unique_attributes": null + }, + "royalty": [], + "extra_metadata": { + "attributes": [], + "utxo_details": { + "distinct_rare_sats": 0, + "satributes": { + "common": { + "count": 8000, + "display_name": "Common", + "description": "Any sat that is not the first sat of its block", + "icon": "https://cdn.simplehash.com/rare_sats/satribute_common.png" + } + }, + "sat_ranges": [ + { + "starting_sat": 574534229491726, + "value": 8000, + "distinct_rare_sats": 0, + "year": "2011", + "subranges": [ + { + "starting_sat": 574534229491726, + "value": 8000, + "sat_types": ["common"] + } + ] + } + ], + "block_number": 834655, + "value": 8000, + "script_pub_key": { + "asm": "", + "desc": "", + "hex": "", + "address": "", + "type": "" + } + }, + "image_original_url": null, + "animation_original_url": null, + "metadata_original_url": null + } + }, + { + "nft_id": "utxo.7893390614d9a5608e39ea637e5794f7564d3fcdd5ad46105f349c2cf45272fb.0", + "chain": "utxo", + "contract_address": "7893390614d9a5608e39ea637e5794f7564d3fcdd5ad46105f349c2cf45272fb", + "token_id": "0", + "name": null, + "description": null, + "previews": { + "image_small_url": null, + "image_medium_url": null, + "image_large_url": null, + "image_opengraph_url": null, + "blurhash": null, + "predominant_color": null + }, + "image_url": null, + "image_properties": null, + "video_url": null, + "video_properties": null, + "audio_url": null, + "audio_properties": null, + "model_url": null, + "model_properties": null, + "other_url": null, + "other_properties": null, + "background_color": null, + "external_url": null, + "created_date": "2024-03-14T11:34:34", + "status": "minted", + "token_count": 1, + "owner_count": 1, + "owners": [ + { + "owerner_address": "weneverknow", + "quantity": 1, + "quantity_string": "1", + "first_acquired_date": "2024-03-14T11:34:34", + "last_acquired_date": "2024-03-14T11:34:34" + } + ], + "contract": { + "type": "UTXO", + "name": null, + "symbol": null, + "deployed_by": null, + "deployed_via_contract": null, + "owned_by": null, + "has_multiple_collections": false + }, + "collection": { + "collection_id": null, + "name": null, + "description": null, + "image_url": null, + "image_properties": null, + "banner_image_url": null, + "category": null, + "is_nsfw": null, + "external_url": null, + "twitter_username": null, + "discord_url": null, + "instagram_username": null, + "medium_username": null, + "telegram_url": null, + "marketplace_pages": [], + "metaplex_mint": null, + "metaplex_candy_machine": null, + "metaplex_first_verified_creator": null, + "spam_score": null, + "floor_prices": [], + "top_bids": [], + "distinct_owner_count": null, + "distinct_nft_count": null, + "total_quantity": null, + "chains": [], + "top_contracts": [], + "collection_royalties": [] + }, + "last_sale": null, + "primary_sale": null, + "first_created": { + "minted_to": "bc1pgtat0n2kavrz4ufhngm2muzxzx6pcmvr4czp089v48u5sgvpd9vqjsuaql", + "quantity": 1, + "quantity_string": "1", + "timestamp": "2024-03-14T11:34:34", + "block_number": 834655, + "transaction": "", + "transaction_initiator": null + }, + "rarity": { + "rank": null, + "score": null, + "unique_attributes": null + }, + "royalty": [], + "extra_metadata": { + "attributes": [], + "utxo_details": { + "distinct_rare_sats": 0, + "satributes": { + "common": { + "count": 9000, + "display_name": "Common", + "description": "Any sat that is not the first sat of its block", + "icon": "https://cdn.simplehash.com/rare_sats/satribute_common.png" + } + }, + "sat_ranges": [ + { + "starting_sat": 574534229482726, + "value": 9000, + "distinct_rare_sats": 0, + "year": "2011", + "subranges": [ + { + "starting_sat": 574534229482726, + "value": 9000, + "sat_types": ["common"] + } + ] + } + ], + "block_number": 834655, + "value": 9000, + "script_pub_key": { + "asm": "", + "desc": "", + "hex": "", + "address": "", + "type": "" + } + }, + "image_original_url": null, + "animation_original_url": null, + "metadata_original_url": null + } + }, + { + "nft_id": "utxo.51fb634f0fefa3441e1a60090d9e292ce1f0803258c2dae818410db4192c89f6.0", + "chain": "utxo", + "contract_address": "51fb634f0fefa3441e1a60090d9e292ce1f0803258c2dae818410db4192c89f6", + "token_id": "0", + "name": null, + "description": null, + "previews": { + "image_small_url": null, + "image_medium_url": null, + "image_large_url": null, + "image_opengraph_url": null, + "blurhash": null, + "predominant_color": null + }, + "image_url": null, + "image_properties": null, + "video_url": null, + "video_properties": null, + "audio_url": null, + "audio_properties": null, + "model_url": null, + "model_properties": null, + "other_url": null, + "other_properties": null, + "background_color": null, + "external_url": null, + "created_date": "2024-03-08T10:37:28", + "status": "minted", + "token_count": 1, + "owner_count": 1, + "owners": [ + { + "owerner_address": "weneverknow", + "quantity": 1, + "quantity_string": "1", + "first_acquired_date": "2024-03-08T10:37:28", + "last_acquired_date": "2024-03-08T10:37:28" + } + ], + "contract": { + "type": "UTXO", + "name": null, + "symbol": null, + "deployed_by": null, + "deployed_via_contract": null, + "owned_by": null, + "has_multiple_collections": false + }, + "collection": { + "collection_id": null, + "name": null, + "description": null, + "image_url": null, + "image_properties": null, + "banner_image_url": null, + "category": null, + "is_nsfw": null, + "external_url": null, + "twitter_username": null, + "discord_url": null, + "instagram_username": null, + "medium_username": null, + "telegram_url": null, + "marketplace_pages": [], + "metaplex_mint": null, + "metaplex_candy_machine": null, + "metaplex_first_verified_creator": null, + "spam_score": null, + "floor_prices": [], + "top_bids": [], + "distinct_owner_count": null, + "distinct_nft_count": null, + "total_quantity": null, + "chains": [], + "top_contracts": [], + "collection_royalties": [] + }, + "last_sale": null, + "primary_sale": null, + "first_created": { + "minted_to": "bc1pgtat0n2kavrz4ufhngm2muzxzx6pcmvr4czp089v48u5sgvpd9vqjsuaql", + "quantity": 1, + "quantity_string": "1", + "timestamp": "2024-03-08T10:37:28", + "block_number": 833719, + "transaction": "", + "transaction_initiator": null + }, + "rarity": { + "rank": null, + "score": null, + "unique_attributes": null + }, + "royalty": [], + "extra_metadata": { + "attributes": [], + "utxo_details": { + "distinct_rare_sats": 0, + "satributes": { + "common": { + "count": 600, + "display_name": "Common", + "description": "Any sat that is not the first sat of its block", + "icon": "https://cdn.simplehash.com/rare_sats/satribute_common.png" + } + }, + "sat_ranges": [ + { + "starting_sat": 1032093336971063, + "value": 600, + "distinct_rare_sats": 0, + "year": "2012", + "subranges": [ + { + "starting_sat": 1032093336971063, + "value": 600, + "sat_types": ["common"] + } + ] + } + ], + "block_number": 833719, + "value": 600, + "script_pub_key": { + "asm": "", + "desc": "", + "hex": "", + "address": "", + "type": "" + } + }, + "image_original_url": null, + "animation_original_url": null, + "metadata_original_url": null + } + }, + { + "nft_id": "bitcoin.51fb634f0fefa3441e1a60090d9e292ce1f0803258c2dae818410db4192c89f6i0", + "chain": "bitcoin", + "contract_address": "51fb634f0fefa3441e1a60090d9e292ce1f0803258c2dae818410db4192c89f6i0", + "token_id": null, + "name": null, + "description": null, + "previews": { + "image_small_url": "https://lh3.googleusercontent.com/3SvffDeKTOIYaByvQqk2pnIlWKsBUcwW_u5jAEdwLGNbRJgjJPWv544atw0tKuR78-xy8YbhDSbNh-zofN4C7AnBCEj6t9TqXA=s250", + "image_medium_url": "https://lh3.googleusercontent.com/3SvffDeKTOIYaByvQqk2pnIlWKsBUcwW_u5jAEdwLGNbRJgjJPWv544atw0tKuR78-xy8YbhDSbNh-zofN4C7AnBCEj6t9TqXA", + "image_large_url": "https://lh3.googleusercontent.com/3SvffDeKTOIYaByvQqk2pnIlWKsBUcwW_u5jAEdwLGNbRJgjJPWv544atw0tKuR78-xy8YbhDSbNh-zofN4C7AnBCEj6t9TqXA=s1000", + "image_opengraph_url": "https://lh3.googleusercontent.com/3SvffDeKTOIYaByvQqk2pnIlWKsBUcwW_u5jAEdwLGNbRJgjJPWv544atw0tKuR78-xy8YbhDSbNh-zofN4C7AnBCEj6t9TqXA=k-w1200-s2400-rj", + "blurhash": "UPFI}DWVl-bGSgoLWVo1l-Woq^WoW.n*WVj[", + "predominant_color": "#84e7db" + }, + "image_url": null, + "image_properties": null, + "video_url": null, + "video_properties": null, + "audio_url": null, + "audio_properties": null, + "model_url": null, + "model_properties": null, + "other_url": "https://ordinals.simplehash.com/content/51fb634f0fefa3441e1a60090d9e292ce1f0803258c2dae818410db4192c89f6i0", + "other_properties": { + "mime_type": "text/html" + }, + "background_color": null, + "external_url": null, + "created_date": "2024-03-08T10:37:28", + "status": "minted", + "token_count": 1, + "owner_count": 1, + "owners": [ + { + "owerner_address": "weneverknow", + "quantity": 1, + "quantity_string": "1", + "first_acquired_date": "2024-03-08T10:37:28", + "last_acquired_date": "2024-03-08T10:37:28" + } + ], + "contract": { + "type": "ORDINALS", + "name": "Inscription #63691311", + "symbol": null, + "deployed_by": null, + "deployed_via_contract": null, + "owned_by": null, + "has_multiple_collections": false + }, + "collection": { + "collection_id": null, + "name": null, + "description": null, + "image_url": null, + "image_properties": null, + "banner_image_url": null, + "category": null, + "is_nsfw": null, + "external_url": null, + "twitter_username": null, + "discord_url": null, + "instagram_username": null, + "medium_username": null, + "telegram_url": null, + "marketplace_pages": [], + "metaplex_mint": null, + "metaplex_candy_machine": null, + "metaplex_first_verified_creator": null, + "spam_score": null, + "floor_prices": [], + "top_bids": [], + "distinct_owner_count": null, + "distinct_nft_count": null, + "total_quantity": null, + "chains": [], + "top_contracts": [], + "collection_royalties": [] + }, + "last_sale": null, + "primary_sale": null, + "first_created": { + "minted_to": "bc1pgtat0n2kavrz4ufhngm2muzxzx6pcmvr4czp089v48u5sgvpd9vqjsuaql", + "quantity": 1, + "quantity_string": "1", + "timestamp": "2024-03-08T10:37:28", + "block_number": 833719, + "transaction": "", + "transaction_initiator": null + }, + "rarity": { + "rank": null, + "score": null, + "unique_attributes": null + }, + "royalty": [], + "extra_metadata": { + "attributes": [], + "ordinal_details": { + "inscription_id": "51fb634f0fefa3441e1a60090d9e292ce1f0803258c2dae818410db4192c89f6i0", + "inscription_number": 63691311, + "content_length": 188, + "content_type": "text/html;charset=utf-8", + "sat_number": 1032093336971063, + "sat_name": "gnqupvpiwdm", + "sat_rarity": "palindrome", + "protocol_name": null, + "protocol_content": null, + "location": "51fb634f0fefa3441e1a60090d9e292ce1f0803258c2dae818410db4192c89f6:0:0", + "output_value": 600, + "parents": null, + "charms": null + }, + "image_original_url": "https://ordinals.simplehash.com/content/51fb634f0fefa3441e1a60090d9e292ce1f0803258c2dae818410db4192c89f6i0", + "animation_original_url": null, + "metadata_original_url": null + } + }, + { + "nft_id": "utxo.b2011509f56ca83378d5e72e413ed5c939cfa4ff3d82680c1b12b5a6b406ac95.0", + "chain": "utxo", + "contract_address": "b2011509f56ca83378d5e72e413ed5c939cfa4ff3d82680c1b12b5a6b406ac95", + "token_id": "0", + "name": null, + "description": null, + "previews": { + "image_small_url": null, + "image_medium_url": null, + "image_large_url": null, + "image_opengraph_url": null, + "blurhash": null, + "predominant_color": null + }, + "image_url": null, + "image_properties": null, + "video_url": null, + "video_properties": null, + "audio_url": null, + "audio_properties": null, + "model_url": null, + "model_properties": null, + "other_url": null, + "other_properties": null, + "background_color": null, + "external_url": null, + "created_date": "2024-03-08T10:33:01", + "status": "minted", + "token_count": 1, + "owner_count": 1, + "owners": [ + { + "owerner_address": "weneverknow", + "quantity": 1, + "quantity_string": "1", + "first_acquired_date": "2024-03-08T10:33:01", + "last_acquired_date": "2024-03-08T10:33:01" + } + ], + "contract": { + "type": "UTXO", + "name": null, + "symbol": null, + "deployed_by": null, + "deployed_via_contract": null, + "owned_by": null, + "has_multiple_collections": false + }, + "collection": { + "collection_id": null, + "name": null, + "description": null, + "image_url": null, + "image_properties": null, + "banner_image_url": null, + "category": null, + "is_nsfw": null, + "external_url": null, + "twitter_username": null, + "discord_url": null, + "instagram_username": null, + "medium_username": null, + "telegram_url": null, + "marketplace_pages": [], + "metaplex_mint": null, + "metaplex_candy_machine": null, + "metaplex_first_verified_creator": null, + "spam_score": null, + "floor_prices": [], + "top_bids": [], + "distinct_owner_count": null, + "distinct_nft_count": null, + "total_quantity": null, + "chains": [], + "top_contracts": [], + "collection_royalties": [] + }, + "last_sale": null, + "primary_sale": null, + "first_created": { + "minted_to": "bc1pgtat0n2kavrz4ufhngm2muzxzx6pcmvr4czp089v48u5sgvpd9vqjsuaql", + "quantity": 1, + "quantity_string": "1", + "timestamp": "2024-03-08T10:33:01", + "block_number": 833718, + "transaction": "", + "transaction_initiator": null + }, + "rarity": { + "rank": null, + "score": null, + "unique_attributes": null + }, + "royalty": [], + "extra_metadata": { + "attributes": [], + "utxo_details": { + "distinct_rare_sats": 0, + "satributes": { + "common": { + "count": 600, + "display_name": "Common", + "description": "Any sat that is not the first sat of its block", + "icon": "https://cdn.simplehash.com/rare_sats/satribute_common.png" + } + }, + "sat_ranges": [ + { + "starting_sat": 1032093336949888, + "value": 600, + "distinct_rare_sats": 0, + "year": "2012", + "subranges": [ + { + "starting_sat": 1032093336949888, + "value": 600, + "sat_types": ["common"] + } + ] + } + ], + "block_number": 833718, + "value": 600, + "script_pub_key": { + "asm": "", + "desc": "", + "hex": "", + "address": "", + "type": "" + } + }, + "image_original_url": null, + "animation_original_url": null, + "metadata_original_url": null + } + }, + { + "nft_id": "bitcoin.b2011509f56ca83378d5e72e413ed5c939cfa4ff3d82680c1b12b5a6b406ac95i0", + "chain": "bitcoin", + "contract_address": "b2011509f56ca83378d5e72e413ed5c939cfa4ff3d82680c1b12b5a6b406ac95i0", + "token_id": null, + "name": null, + "description": null, + "previews": { + "image_small_url": "https://lh3.googleusercontent.com/xTE3sfYiK7gxq_cIVzNTdy3pWfOOOondhBMWCPNFt69EItUBBdj_EWA_xWw2tK0Z2Kr8JqjoHv-0YsBP-NoF44RN_hkxikyCkN4=s250", + "image_medium_url": "https://lh3.googleusercontent.com/xTE3sfYiK7gxq_cIVzNTdy3pWfOOOondhBMWCPNFt69EItUBBdj_EWA_xWw2tK0Z2Kr8JqjoHv-0YsBP-NoF44RN_hkxikyCkN4", + "image_large_url": "https://lh3.googleusercontent.com/xTE3sfYiK7gxq_cIVzNTdy3pWfOOOondhBMWCPNFt69EItUBBdj_EWA_xWw2tK0Z2Kr8JqjoHv-0YsBP-NoF44RN_hkxikyCkN4=s1000", + "image_opengraph_url": "https://lh3.googleusercontent.com/xTE3sfYiK7gxq_cIVzNTdy3pWfOOOondhBMWCPNFt69EItUBBdj_EWA_xWw2tK0Z2Kr8JqjoHv-0YsBP-NoF44RN_hkxikyCkN4=k-w1200-s2400-rj", + "blurhash": "UDA]pJRk0foxMzoLtPbH0ModkqR*%KWXM}oe", + "predominant_color": "#e28c3b" + }, + "image_url": null, + "image_properties": null, + "video_url": null, + "video_properties": null, + "audio_url": null, + "audio_properties": null, + "model_url": null, + "model_properties": null, + "other_url": "https://ordinals.simplehash.com/content/b2011509f56ca83378d5e72e413ed5c939cfa4ff3d82680c1b12b5a6b406ac95i0", + "other_properties": { + "mime_type": "text/html" + }, + "background_color": null, + "external_url": null, + "created_date": "2024-03-08T10:33:01", + "status": "minted", + "token_count": 1, + "owner_count": 1, + "owners": [ + { + "owerner_address": "weneverknow", + "quantity": 1, + "quantity_string": "1", + "first_acquired_date": "2024-03-08T10:33:01", + "last_acquired_date": "2024-03-08T10:33:01" + } + ], + "contract": { + "type": "ORDINALS", + "name": "Inscription #63690060", + "symbol": null, + "deployed_by": null, + "deployed_via_contract": null, + "owned_by": null, + "has_multiple_collections": false + }, + "collection": { + "collection_id": null, + "name": null, + "description": null, + "image_url": null, + "image_properties": null, + "banner_image_url": null, + "category": null, + "is_nsfw": null, + "external_url": null, + "twitter_username": null, + "discord_url": null, + "instagram_username": null, + "medium_username": null, + "telegram_url": null, + "marketplace_pages": [], + "metaplex_mint": null, + "metaplex_candy_machine": null, + "metaplex_first_verified_creator": null, + "spam_score": null, + "floor_prices": [], + "top_bids": [], + "distinct_owner_count": null, + "distinct_nft_count": null, + "total_quantity": null, + "chains": [], + "top_contracts": [], + "collection_royalties": [] + }, + "last_sale": null, + "primary_sale": null, + "first_created": { + "minted_to": "bc1pgtat0n2kavrz4ufhngm2muzxzx6pcmvr4czp089v48u5sgvpd9vqjsuaql", + "quantity": 1, + "quantity_string": "1", + "timestamp": "2024-03-08T10:33:01", + "block_number": 833718, + "transaction": "", + "transaction_initiator": null + }, + "rarity": { + "rank": null, + "score": null, + "unique_attributes": null + }, + "royalty": [], + "extra_metadata": { + "attributes": [], + "ordinal_details": { + "inscription_id": "b2011509f56ca83378d5e72e413ed5c939cfa4ff3d82680c1b12b5a6b406ac95i0", + "inscription_number": 63690060, + "content_length": 187, + "content_type": "text/html;charset=utf-8", + "sat_number": 1032093336949888, + "sat_name": "gnqupvpkblx", + "sat_rarity": "common", + "protocol_name": null, + "protocol_content": null, + "location": "b2011509f56ca83378d5e72e413ed5c939cfa4ff3d82680c1b12b5a6b406ac95:0:0", + "output_value": 600, + "parents": null, + "charms": null + }, + "image_original_url": "https://ordinals.simplehash.com/content/b2011509f56ca83378d5e72e413ed5c939cfa4ff3d82680c1b12b5a6b406ac95i0", + "animation_original_url": null, + "metadata_original_url": null + } + }, + { + "nft_id": "utxo.398901189471c069dd74068a2884e02829c831432e705b28a0c337696e681b97.0", + "chain": "utxo", + "contract_address": "398901189471c069dd74068a2884e02829c831432e705b28a0c337696e681b97", + "token_id": "0", + "name": null, + "description": null, + "previews": { + "image_small_url": null, + "image_medium_url": null, + "image_large_url": null, + "image_opengraph_url": null, + "blurhash": null, + "predominant_color": null + }, + "image_url": null, + "image_properties": null, + "video_url": null, + "video_properties": null, + "audio_url": null, + "audio_properties": null, + "model_url": null, + "model_properties": null, + "other_url": null, + "other_properties": null, + "background_color": null, + "external_url": null, + "created_date": "2024-03-08T10:06:46", + "status": "minted", + "token_count": 1, + "owner_count": 1, + "owners": [ + { + "owerner_address": "weneverknow", + "quantity": 1, + "quantity_string": "1", + "first_acquired_date": "2024-03-08T10:06:46", + "last_acquired_date": "2024-03-08T10:06:46" + } + ], + "contract": { + "type": "UTXO", + "name": null, + "symbol": null, + "deployed_by": null, + "deployed_via_contract": null, + "owned_by": null, + "has_multiple_collections": false + }, + "collection": { + "collection_id": null, + "name": null, + "description": null, + "image_url": null, + "image_properties": null, + "banner_image_url": null, + "category": null, + "is_nsfw": null, + "external_url": null, + "twitter_username": null, + "discord_url": null, + "instagram_username": null, + "medium_username": null, + "telegram_url": null, + "marketplace_pages": [], + "metaplex_mint": null, + "metaplex_candy_machine": null, + "metaplex_first_verified_creator": null, + "spam_score": null, + "floor_prices": [], + "top_bids": [], + "distinct_owner_count": null, + "distinct_nft_count": null, + "total_quantity": null, + "chains": [], + "top_contracts": [], + "collection_royalties": [] + }, + "last_sale": null, + "primary_sale": null, + "first_created": { + "minted_to": "bc1pgtat0n2kavrz4ufhngm2muzxzx6pcmvr4czp089v48u5sgvpd9vqjsuaql", + "quantity": 1, + "quantity_string": "1", + "timestamp": "2024-03-08T10:06:46", + "block_number": 833714, + "transaction": "", + "transaction_initiator": null + }, + "rarity": { + "rank": null, + "score": null, + "unique_attributes": null + }, + "royalty": [], + "extra_metadata": { + "attributes": [], + "utxo_details": { + "distinct_rare_sats": 0, + "satributes": { + "common": { + "count": 600, + "display_name": "Common", + "description": "Any sat that is not the first sat of its block", + "icon": "https://cdn.simplehash.com/rare_sats/satribute_common.png" + } + }, + "sat_ranges": [ + { + "starting_sat": 1032093336943299, + "value": 600, + "distinct_rare_sats": 0, + "year": "2012", + "subranges": [ + { + "starting_sat": 1032093336943299, + "value": 600, + "sat_types": ["common"] + } + ] + } + ], + "block_number": 833714, + "value": 600, + "script_pub_key": { + "asm": "", + "desc": "", + "hex": "", + "address": "", + "type": "" + } + }, + "image_original_url": null, + "animation_original_url": null, + "metadata_original_url": null + } + }, + { + "nft_id": "bitcoin.398901189471c069dd74068a2884e02829c831432e705b28a0c337696e681b97i0", + "chain": "bitcoin", + "contract_address": "398901189471c069dd74068a2884e02829c831432e705b28a0c337696e681b97i0", + "token_id": null, + "name": null, + "description": null, + "previews": { + "image_small_url": null, + "image_medium_url": null, + "image_large_url": null, + "image_opengraph_url": null, + "blurhash": null, + "predominant_color": null + }, + "image_url": null, + "image_properties": null, + "video_url": null, + "video_properties": null, + "audio_url": null, + "audio_properties": null, + "model_url": null, + "model_properties": null, + "other_url": "https://ordinals.simplehash.com/content/398901189471c069dd74068a2884e02829c831432e705b28a0c337696e681b97i0", + "other_properties": { + "mime_type": "text/html" + }, + "background_color": null, + "external_url": null, + "created_date": "2024-03-08T10:06:46", + "status": "minted", + "token_count": 1, + "owner_count": 1, + "owners": [ + { + "owerner_address": "weneverknow", + "quantity": 1, + "quantity_string": "1", + "first_acquired_date": "2024-03-08T10:06:46", + "last_acquired_date": "2024-03-08T10:06:46" + } + ], + "contract": { + "type": "ORDINALS", + "name": "Inscription #63677178", + "symbol": null, + "deployed_by": null, + "deployed_via_contract": null, + "owned_by": null, + "has_multiple_collections": false + }, + "collection": { + "collection_id": "30bda699616f2f7c4d8269825498578d", + "name": "Luminescates", + "description": "Introducing Luminescates!Explore an infinite variety of radiant beings that enchant, astonish, and enthrall.Immerse yourself in the vast expanse of boundless Luminescates—what wonders will you discover‽Highly interactive and endlessly entertaining, explore the mesmerising combinations and lose yourself in infinity!Controls:\"1\" create new Luminescate.\"double click\" change frequency\"click & drag\" change shape/size\"space bar\" swap Lemniscate typeLuminescence + Lemniscates = Luminescates.", + "image_url": "https://lh3.googleusercontent.com/6XhTag87_zRPXjFlVmYfj1tUr_iye2wWse4wOX3IPyQec8Adzs3VOk_I4EZDathyzmwV_rj95lCSsqw7wpzKIViWJ51wAg1_lPc", + "image_properties": { + "width": 502, + "height": 512, + "mime_type": "image/jpeg" + }, + "banner_image_url": null, + "category": null, + "is_nsfw": null, + "external_url": null, + "twitter_username": "0xJANK", + "discord_url": "https://discord.gg/4rQ9cv4JeF", + "instagram_username": null, + "medium_username": null, + "telegram_url": null, + "marketplace_pages": [ + { + "marketplace_id": "magiceden", + "marketplace_name": "Magic Eden", + "marketplace_collection_id": "luminescates", + "nft_url": "https://magiceden.io/ordinals/item-details/398901189471c069dd74068a2884e02829c831432e705b28a0c337696e681b97i0", + "collection_url": "https://magiceden.io/ordinals/marketplace/luminescates", + "verified": null + } + ], + "metaplex_mint": null, + "metaplex_candy_machine": null, + "metaplex_first_verified_creator": null, + "spam_score": 0, + "floor_prices": [ + { + "marketplace_id": "magiceden", + "marketplace_name": "Magic Eden", + "value": 13690, + "payment_token": { + "payment_token_id": "bitcoin.native", + "name": "Bitcoin", + "symbol": "BTC", + "address": null, + "decimals": 8 + }, + "value_usd_cents": 796 + } + ], + "top_bids": [], + "distinct_owner_count": 211, + "distinct_nft_count": 332, + "total_quantity": 332, + "chains": ["bitcoin"], + "top_contracts": [], + "collection_royalties": [] + }, + "last_sale": null, + "primary_sale": null, + "first_created": { + "minted_to": "bc1pgtat0n2kavrz4ufhngm2muzxzx6pcmvr4czp089v48u5sgvpd9vqjsuaql", + "quantity": 1, + "quantity_string": "1", + "timestamp": "2024-03-08T10:06:46", + "block_number": 833714, + "transaction": "", + "transaction_initiator": null + }, + "rarity": { + "rank": null, + "score": null, + "unique_attributes": null + }, + "royalty": [], + "extra_metadata": { + "attributes": [], + "ordinal_details": { + "inscription_id": "398901189471c069dd74068a2884e02829c831432e705b28a0c337696e681b97i0", + "inscription_number": 63677178, + "content_length": 188, + "content_type": "text/html;charset=utf-8", + "sat_number": 1032093336943299, + "sat_name": "gnqupvpklfi", + "sat_rarity": "common", + "protocol_name": null, + "protocol_content": null, + "location": "398901189471c069dd74068a2884e02829c831432e705b28a0c337696e681b97:0:0", + "output_value": 600, + "parents": null, + "charms": null + }, + "image_original_url": "https://ordinals.simplehash.com/content/398901189471c069dd74068a2884e02829c831432e705b28a0c337696e681b97i0", + "animation_original_url": null, + "metadata_original_url": null + } + }, + { + "nft_id": "utxo.68f11cb8770c6e761a5763bb47f00b368e03a4960046f441617c46edad2b0215.5", + "chain": "utxo", + "contract_address": "68f11cb8770c6e761a5763bb47f00b368e03a4960046f441617c46edad2b0215", + "token_id": "5", + "name": null, + "description": null, + "previews": { + "image_small_url": null, + "image_medium_url": null, + "image_large_url": null, + "image_opengraph_url": null, + "blurhash": null, + "predominant_color": null + }, + "image_url": null, + "image_properties": null, + "video_url": null, + "video_properties": null, + "audio_url": null, + "audio_properties": null, + "model_url": null, + "model_properties": null, + "other_url": null, + "other_properties": null, + "background_color": null, + "external_url": null, + "created_date": "2024-03-08T09:47:36", + "status": "minted", + "token_count": 1, + "owner_count": 1, + "owners": [ + { + "owerner_address": "weneverknow", + "quantity": 1, + "quantity_string": "1", + "first_acquired_date": "2024-03-08T09:47:36", + "last_acquired_date": "2024-03-08T09:47:36" + } + ], + "contract": { + "type": "UTXO", + "name": null, + "symbol": null, + "deployed_by": null, + "deployed_via_contract": null, + "owned_by": null, + "has_multiple_collections": false + }, + "collection": { + "collection_id": "0123456789abcdeffedcba9876543210", + "name": "Rare Sats", + "description": "Rare Sats are attributes, or \"satributes,\" ascribed to different types of sats. Sats are the smallest unit of a Bitcoin, and satributes commemorate special moments like when a sat was mined or used in a transaction.", + "image_url": null, + "image_properties": null, + "banner_image_url": null, + "category": null, + "is_nsfw": null, + "external_url": null, + "twitter_username": null, + "discord_url": null, + "instagram_username": null, + "medium_username": null, + "telegram_url": null, + "marketplace_pages": [], + "metaplex_mint": null, + "metaplex_candy_machine": null, + "metaplex_first_verified_creator": null, + "spam_score": null, + "floor_prices": [], + "top_bids": [], + "distinct_owner_count": 5346731, + "distinct_nft_count": 7558913, + "total_quantity": 7558900, + "chains": ["utxo"], + "top_contracts": [], + "collection_royalties": [] + }, + "last_sale": null, + "primary_sale": null, + "first_created": { + "minted_to": "bc1pgtat0n2kavrz4ufhngm2muzxzx6pcmvr4czp089v48u5sgvpd9vqjsuaql", + "quantity": 1, + "quantity_string": "1", + "timestamp": "2024-03-08T09:47:36", + "block_number": 833711, + "transaction": "", + "transaction_initiator": null + }, + "rarity": { + "rank": null, + "score": null, + "unique_attributes": null + }, + "royalty": [], + "extra_metadata": { + "attributes": [], + "utxo_details": { + "distinct_rare_sats": 330, + "satributes": { + "block_78": { + "count": 330, + "display_name": "Block 78", + "description": "Sats mined by Hal Finney in block 78 which was the first block mined by someone other than Satoshi", + "icon": "https://cdn.simplehash.com/rare_sats/satribute_block78.png" + }, + "vintage": { + "count": 330, + "display_name": "Vintage", + "description": "Sats mined in the first 1,000 blocks", + "icon": "https://cdn.simplehash.com/rare_sats/satribute_vintage.png" + } + }, + "sat_ranges": [ + { + "starting_sat": 392582274563, + "value": 330, + "distinct_rare_sats": 330, + "year": "2009", + "subranges": [ + { + "starting_sat": 392582274563, + "value": 330, + "sat_types": ["block_78", "vintage"] + } + ] + } + ], + "block_number": 833711, + "value": 330, + "script_pub_key": { + "asm": "", + "desc": "", + "hex": "", + "address": "", + "type": "" + } + }, + "image_original_url": null, + "animation_original_url": null, + "metadata_original_url": null + } + }, + { + "nft_id": "utxo.68f11cb8770c6e761a5763bb47f00b368e03a4960046f441617c46edad2b0215.4", + "chain": "utxo", + "contract_address": "68f11cb8770c6e761a5763bb47f00b368e03a4960046f441617c46edad2b0215", + "token_id": "4", + "name": null, + "description": null, + "previews": { + "image_small_url": null, + "image_medium_url": null, + "image_large_url": null, + "image_opengraph_url": null, + "blurhash": null, + "predominant_color": null + }, + "image_url": null, + "image_properties": null, + "video_url": null, + "video_properties": null, + "audio_url": null, + "audio_properties": null, + "model_url": null, + "model_properties": null, + "other_url": null, + "other_properties": null, + "background_color": null, + "external_url": null, + "created_date": "2024-03-08T09:47:36", + "status": "minted", + "token_count": 1, + "owner_count": 1, + "owners": [ + { + "owerner_address": "weneverknow", + "quantity": 1, + "quantity_string": "1", + "first_acquired_date": "2024-03-08T09:47:36", + "last_acquired_date": "2024-03-08T09:47:36" + } + ], + "contract": { + "type": "UTXO", + "name": null, + "symbol": null, + "deployed_by": null, + "deployed_via_contract": null, + "owned_by": null, + "has_multiple_collections": false + }, + "collection": { + "collection_id": "0123456789abcdeffedcba9876543210", + "name": "Rare Sats", + "description": "Rare Sats are attributes, or \"satributes,\" ascribed to different types of sats. Sats are the smallest unit of a Bitcoin, and satributes commemorate special moments like when a sat was mined or used in a transaction.", + "image_url": null, + "image_properties": null, + "banner_image_url": null, + "category": null, + "is_nsfw": null, + "external_url": null, + "twitter_username": null, + "discord_url": null, + "instagram_username": null, + "medium_username": null, + "telegram_url": null, + "marketplace_pages": [], + "metaplex_mint": null, + "metaplex_candy_machine": null, + "metaplex_first_verified_creator": null, + "spam_score": null, + "floor_prices": [], + "top_bids": [], + "distinct_owner_count": 5346731, + "distinct_nft_count": 7558913, + "total_quantity": 7558900, + "chains": ["utxo"], + "top_contracts": [], + "collection_royalties": [] + }, + "last_sale": null, + "primary_sale": null, + "first_created": { + "minted_to": "bc1pgtat0n2kavrz4ufhngm2muzxzx6pcmvr4czp089v48u5sgvpd9vqjsuaql", + "quantity": 1, + "quantity_string": "1", + "timestamp": "2024-03-08T09:47:36", + "block_number": 833711, + "transaction": "", + "transaction_initiator": null + }, + "rarity": { + "rank": null, + "score": null, + "unique_attributes": null + }, + "royalty": [], + "extra_metadata": { + "attributes": [], + "utxo_details": { + "distinct_rare_sats": 330, + "satributes": { + "vintage": { + "count": 330, + "display_name": "Vintage", + "description": "Sats mined in the first 1,000 blocks", + "icon": "https://cdn.simplehash.com/rare_sats/satribute_vintage.png" + } + }, + "sat_ranges": [ + { + "starting_sat": 4834087454144, + "value": 330, + "distinct_rare_sats": 330, + "year": "2009", + "subranges": [ + { + "starting_sat": 4834087454144, + "value": 330, + "sat_types": ["vintage"] + } + ] + } + ], + "block_number": 833711, + "value": 330, + "script_pub_key": { + "asm": "", + "desc": "", + "hex": "", + "address": "", + "type": "" + } + }, + "image_original_url": null, + "animation_original_url": null, + "metadata_original_url": null + } + }, + { + "nft_id": "utxo.68f11cb8770c6e761a5763bb47f00b368e03a4960046f441617c46edad2b0215.3", + "chain": "utxo", + "contract_address": "68f11cb8770c6e761a5763bb47f00b368e03a4960046f441617c46edad2b0215", + "token_id": "3", + "name": null, + "description": null, + "previews": { + "image_small_url": null, + "image_medium_url": null, + "image_large_url": null, + "image_opengraph_url": null, + "blurhash": null, + "predominant_color": null + }, + "image_url": null, + "image_properties": null, + "video_url": null, + "video_properties": null, + "audio_url": null, + "audio_properties": null, + "model_url": null, + "model_properties": null, + "other_url": null, + "other_properties": null, + "background_color": null, + "external_url": null, + "created_date": "2024-03-08T09:47:36", + "status": "minted", + "token_count": 1, + "owner_count": 1, + "owners": [ + { + "owerner_address": "weneverknow", + "quantity": 1, + "quantity_string": "1", + "first_acquired_date": "2024-03-08T09:47:36", + "last_acquired_date": "2024-03-08T09:47:36" + } + ], + "contract": { + "type": "UTXO", + "name": null, + "symbol": null, + "deployed_by": null, + "deployed_via_contract": null, + "owned_by": null, + "has_multiple_collections": false + }, + "collection": { + "collection_id": "0123456789abcdeffedcba9876543210", + "name": "Rare Sats", + "description": "Rare Sats are attributes, or \"satributes,\" ascribed to different types of sats. Sats are the smallest unit of a Bitcoin, and satributes commemorate special moments like when a sat was mined or used in a transaction.", + "image_url": null, + "image_properties": null, + "banner_image_url": null, + "category": null, + "is_nsfw": null, + "external_url": null, + "twitter_username": null, + "discord_url": null, + "instagram_username": null, + "medium_username": null, + "telegram_url": null, + "marketplace_pages": [], + "metaplex_mint": null, + "metaplex_candy_machine": null, + "metaplex_first_verified_creator": null, + "spam_score": null, + "floor_prices": [], + "top_bids": [], + "distinct_owner_count": 5346731, + "distinct_nft_count": 7558913, + "total_quantity": 7558900, + "chains": ["utxo"], + "top_contracts": [], + "collection_royalties": [] + }, + "last_sale": null, + "primary_sale": null, + "first_created": { + "minted_to": "bc1pgtat0n2kavrz4ufhngm2muzxzx6pcmvr4czp089v48u5sgvpd9vqjsuaql", + "quantity": 1, + "quantity_string": "1", + "timestamp": "2024-03-08T09:47:36", + "block_number": 833711, + "transaction": "", + "transaction_initiator": null + }, + "rarity": { + "rank": null, + "score": null, + "unique_attributes": null + }, + "royalty": [], + "extra_metadata": { + "attributes": [], + "utxo_details": { + "distinct_rare_sats": 330, + "satributes": { + "pizza": { + "count": 330, + "display_name": "Pizza", + "description": "Sat from the 10,000 Bitcoins used to purchase two Papa John's pizzas on May 22, 2010", + "icon": "https://cdn.simplehash.com/rare_sats/satribute_pizza.png" + } + }, + "sat_ranges": [ + { + "starting_sat": 265675444186667, + "value": 330, + "distinct_rare_sats": 330, + "year": "2010", + "subranges": [ + { + "starting_sat": 265675444186667, + "value": 330, + "sat_types": ["pizza"] + } + ] + } + ], + "block_number": 833711, + "value": 330, + "script_pub_key": { + "asm": "", + "desc": "", + "hex": "", + "address": "", + "type": "" + } + }, + "image_original_url": null, + "animation_original_url": null, + "metadata_original_url": null + } + }, + { + "nft_id": "utxo.68f11cb8770c6e761a5763bb47f00b368e03a4960046f441617c46edad2b0215.2", + "chain": "utxo", + "contract_address": "68f11cb8770c6e761a5763bb47f00b368e03a4960046f441617c46edad2b0215", + "token_id": "2", + "name": null, + "description": null, + "previews": { + "image_small_url": null, + "image_medium_url": null, + "image_large_url": null, + "image_opengraph_url": null, + "blurhash": null, + "predominant_color": null + }, + "image_url": null, + "image_properties": null, + "video_url": null, + "video_properties": null, + "audio_url": null, + "audio_properties": null, + "model_url": null, + "model_properties": null, + "other_url": null, + "other_properties": null, + "background_color": null, + "external_url": null, + "created_date": "2024-03-08T09:47:36", + "status": "minted", + "token_count": 1, + "owner_count": 1, + "owners": [ + { + "owerner_address": "weneverknow", + "quantity": 1, + "quantity_string": "1", + "first_acquired_date": "2024-03-08T09:47:36", + "last_acquired_date": "2024-03-08T09:47:36" + } + ], + "contract": { + "type": "UTXO", + "name": null, + "symbol": null, + "deployed_by": null, + "deployed_via_contract": null, + "owned_by": null, + "has_multiple_collections": false + }, + "collection": { + "collection_id": "0123456789abcdeffedcba9876543210", + "name": "Rare Sats", + "description": "Rare Sats are attributes, or \"satributes,\" ascribed to different types of sats. Sats are the smallest unit of a Bitcoin, and satributes commemorate special moments like when a sat was mined or used in a transaction.", + "image_url": null, + "image_properties": null, + "banner_image_url": null, + "category": null, + "is_nsfw": null, + "external_url": null, + "twitter_username": null, + "discord_url": null, + "instagram_username": null, + "medium_username": null, + "telegram_url": null, + "marketplace_pages": [], + "metaplex_mint": null, + "metaplex_candy_machine": null, + "metaplex_first_verified_creator": null, + "spam_score": null, + "floor_prices": [], + "top_bids": [], + "distinct_owner_count": 5346731, + "distinct_nft_count": 7558913, + "total_quantity": 7558900, + "chains": ["utxo"], + "top_contracts": [], + "collection_royalties": [] + }, + "last_sale": null, + "primary_sale": null, + "first_created": { + "minted_to": "bc1pgtat0n2kavrz4ufhngm2muzxzx6pcmvr4czp089v48u5sgvpd9vqjsuaql", + "quantity": 1, + "quantity_string": "1", + "timestamp": "2024-03-08T09:47:36", + "block_number": 833711, + "transaction": "", + "transaction_initiator": null + }, + "rarity": { + "rank": null, + "score": null, + "unique_attributes": null + }, + "royalty": [], + "extra_metadata": { + "attributes": [], + "utxo_details": { + "distinct_rare_sats": 547, + "satributes": { + "jpeg": { + "count": 547, + "display_name": "JPEG", + "description": "Sats involved in the possible first bitcoin trade for an image on February 24, 2010", + "icon": "https://cdn.simplehash.com/rare_sats/satribute_jpeg.png" + } + }, + "sat_ranges": [ + { + "starting_sat": 152060443363905, + "value": 547, + "distinct_rare_sats": 547, + "year": "2009", + "subranges": [ + { + "starting_sat": 152060443363905, + "value": 547, + "sat_types": ["jpeg"] + } + ] + } + ], + "block_number": 833711, + "value": 547, + "script_pub_key": { + "asm": "", + "desc": "", + "hex": "", + "address": "", + "type": "" + } + }, + "image_original_url": null, + "animation_original_url": null, + "metadata_original_url": null + } + }, + { + "nft_id": "utxo.68f11cb8770c6e761a5763bb47f00b368e03a4960046f441617c46edad2b0215.1", + "chain": "utxo", + "contract_address": "68f11cb8770c6e761a5763bb47f00b368e03a4960046f441617c46edad2b0215", + "token_id": "1", + "name": null, + "description": null, + "previews": { + "image_small_url": null, + "image_medium_url": null, + "image_large_url": null, + "image_opengraph_url": null, + "blurhash": null, + "predominant_color": null + }, + "image_url": null, + "image_properties": null, + "video_url": null, + "video_properties": null, + "audio_url": null, + "audio_properties": null, + "model_url": null, + "model_properties": null, + "other_url": null, + "other_properties": null, + "background_color": null, + "external_url": null, + "created_date": "2024-03-08T09:47:36", + "status": "minted", + "token_count": 1, + "owner_count": 1, + "owners": [ + { + "owerner_address": "weneverknow", + "quantity": 1, + "quantity_string": "1", + "first_acquired_date": "2024-03-08T09:47:36", + "last_acquired_date": "2024-03-08T09:47:36" + } + ], + "contract": { + "type": "UTXO", + "name": null, + "symbol": null, + "deployed_by": null, + "deployed_via_contract": null, + "owned_by": null, + "has_multiple_collections": false + }, + "collection": { + "collection_id": "0123456789abcdeffedcba9876543210", + "name": "Rare Sats", + "description": "Rare Sats are attributes, or \"satributes,\" ascribed to different types of sats. Sats are the smallest unit of a Bitcoin, and satributes commemorate special moments like when a sat was mined or used in a transaction.", + "image_url": null, + "image_properties": null, + "banner_image_url": null, + "category": null, + "is_nsfw": null, + "external_url": null, + "twitter_username": null, + "discord_url": null, + "instagram_username": null, + "medium_username": null, + "telegram_url": null, + "marketplace_pages": [], + "metaplex_mint": null, + "metaplex_candy_machine": null, + "metaplex_first_verified_creator": null, + "spam_score": null, + "floor_prices": [], + "top_bids": [], + "distinct_owner_count": 5346731, + "distinct_nft_count": 7558913, + "total_quantity": 7558900, + "chains": ["utxo"], + "top_contracts": [], + "collection_royalties": [] + }, + "last_sale": null, + "primary_sale": null, + "first_created": { + "minted_to": "bc1pgtat0n2kavrz4ufhngm2muzxzx6pcmvr4czp089v48u5sgvpd9vqjsuaql", + "quantity": 1, + "quantity_string": "1", + "timestamp": "2024-03-08T09:47:36", + "block_number": 833711, + "transaction": "", + "transaction_initiator": null + }, + "rarity": { + "rank": null, + "score": null, + "unique_attributes": null + }, + "royalty": [], + "extra_metadata": { + "attributes": [], + "utxo_details": { + "distinct_rare_sats": 555, + "satributes": { + "nakamoto": { + "count": 555, + "display_name": "Nakamoto", + "description": "Sat mined by Satoshi Nakamoto", + "icon": "https://cdn.simplehash.com/rare_sats/satribute_nakamoto.png" + } + }, + "sat_ranges": [ + { + "starting_sat": 95469010861290, + "value": 555, + "distinct_rare_sats": 555, + "year": "2009", + "subranges": [ + { + "starting_sat": 95469010861290, + "value": 555, + "sat_types": ["nakamoto"] + } + ] + } + ], + "block_number": 833711, + "value": 555, + "script_pub_key": { + "asm": "", + "desc": "", + "hex": "", + "address": "", + "type": "" + } + }, + "image_original_url": null, + "animation_original_url": null, + "metadata_original_url": null + } + }, + { + "nft_id": "utxo.ce4b148f42e96e30110e666952aeeff6b66a491f7534066076197ff04030318c.1", + "chain": "utxo", + "contract_address": "ce4b148f42e96e30110e666952aeeff6b66a491f7534066076197ff04030318c", + "token_id": "1", + "name": null, + "description": null, + "previews": { + "image_small_url": null, + "image_medium_url": null, + "image_large_url": null, + "image_opengraph_url": null, + "blurhash": null, + "predominant_color": null + }, + "image_url": null, + "image_properties": null, + "video_url": null, + "video_properties": null, + "audio_url": null, + "audio_properties": null, + "model_url": null, + "model_properties": null, + "other_url": null, + "other_properties": null, + "background_color": null, + "external_url": null, + "created_date": "2023-12-11T22:53:55", + "status": "minted", + "token_count": 1, + "owner_count": 1, + "owners": [ + { + "owerner_address": "weneverknow", + "quantity": 1, + "quantity_string": "1", + "first_acquired_date": "2023-12-11T22:53:55", + "last_acquired_date": "2023-12-11T22:53:55" + } + ], + "contract": { + "type": "UTXO", + "name": null, + "symbol": null, + "deployed_by": null, + "deployed_via_contract": null, + "owned_by": null, + "has_multiple_collections": false + }, + "collection": { + "collection_id": "0123456789abcdeffedcba9876543210", + "name": "Rare Sats", + "description": "Rare Sats are attributes, or \"satributes,\" ascribed to different types of sats. Sats are the smallest unit of a Bitcoin, and satributes commemorate special moments like when a sat was mined or used in a transaction.", + "image_url": null, + "image_properties": null, + "banner_image_url": null, + "category": null, + "is_nsfw": null, + "external_url": null, + "twitter_username": null, + "discord_url": null, + "instagram_username": null, + "medium_username": null, + "telegram_url": null, + "marketplace_pages": [], + "metaplex_mint": null, + "metaplex_candy_machine": null, + "metaplex_first_verified_creator": null, + "spam_score": null, + "floor_prices": [], + "top_bids": [], + "distinct_owner_count": 5346731, + "distinct_nft_count": 7558913, + "total_quantity": 7558900, + "chains": ["utxo"], + "top_contracts": [], + "collection_royalties": [] + }, + "last_sale": null, + "primary_sale": null, + "first_created": { + "minted_to": "bc1pgtat0n2kavrz4ufhngm2muzxzx6pcmvr4czp089v48u5sgvpd9vqjsuaql", + "quantity": 1, + "quantity_string": "1", + "timestamp": "2023-12-11T22:53:55", + "block_number": 820769, + "transaction": "", + "transaction_initiator": null + }, + "rarity": { + "rank": null, + "score": null, + "unique_attributes": null + }, + "royalty": [], + "extra_metadata": { + "attributes": [], + "utxo_details": { + "distinct_rare_sats": 835, + "satributes": { + "pizza": { + "count": 835, + "display_name": "Pizza", + "description": "Sat from the 10,000 Bitcoins used to purchase two Papa John's pizzas on May 22, 2010", + "icon": "https://cdn.simplehash.com/rare_sats/satribute_pizza.png" + } + }, + "sat_ranges": [ + { + "starting_sat": 157020560807939, + "value": 835, + "distinct_rare_sats": 835, + "year": "2009", + "subranges": [ + { + "starting_sat": 157020560807939, + "value": 835, + "sat_types": ["pizza"] + } + ] + } + ], + "block_number": 820769, + "value": 835, + "script_pub_key": { + "asm": "", + "desc": "", + "hex": "", + "address": "", + "type": "" + } + }, + "image_original_url": null, + "animation_original_url": null, + "metadata_original_url": null + } + }, + { + "nft_id": "utxo.adca6501891f2cfce159802f6a2fd798460b004924c5aa5c3352cec4dafa30ba.1", + "chain": "utxo", + "contract_address": "adca6501891f2cfce159802f6a2fd798460b004924c5aa5c3352cec4dafa30ba", + "token_id": "1", + "name": null, + "description": null, + "previews": { + "image_small_url": null, + "image_medium_url": null, + "image_large_url": null, + "image_opengraph_url": null, + "blurhash": null, + "predominant_color": null + }, + "image_url": null, + "image_properties": null, + "video_url": null, + "video_properties": null, + "audio_url": null, + "audio_properties": null, + "model_url": null, + "model_properties": null, + "other_url": null, + "other_properties": null, + "background_color": null, + "external_url": null, + "created_date": "2023-12-11T15:39:00", + "status": "minted", + "token_count": 1, + "owner_count": 1, + "owners": [ + { + "owerner_address": "weneverknow", + "quantity": 1, + "quantity_string": "1", + "first_acquired_date": "2023-12-11T15:39:00", + "last_acquired_date": "2023-12-11T15:39:00" + } + ], + "contract": { + "type": "UTXO", + "name": null, + "symbol": null, + "deployed_by": null, + "deployed_via_contract": null, + "owned_by": null, + "has_multiple_collections": false + }, + "collection": { + "collection_id": "0123456789abcdeffedcba9876543210", + "name": "Rare Sats", + "description": "Rare Sats are attributes, or \"satributes,\" ascribed to different types of sats. Sats are the smallest unit of a Bitcoin, and satributes commemorate special moments like when a sat was mined or used in a transaction.", + "image_url": null, + "image_properties": null, + "banner_image_url": null, + "category": null, + "is_nsfw": null, + "external_url": null, + "twitter_username": null, + "discord_url": null, + "instagram_username": null, + "medium_username": null, + "telegram_url": null, + "marketplace_pages": [], + "metaplex_mint": null, + "metaplex_candy_machine": null, + "metaplex_first_verified_creator": null, + "spam_score": null, + "floor_prices": [], + "top_bids": [], + "distinct_owner_count": 5346731, + "distinct_nft_count": 7558913, + "total_quantity": 7558900, + "chains": ["utxo"], + "top_contracts": [], + "collection_royalties": [] + }, + "last_sale": null, + "primary_sale": null, + "first_created": { + "minted_to": "bc1pgtat0n2kavrz4ufhngm2muzxzx6pcmvr4czp089v48u5sgvpd9vqjsuaql", + "quantity": 1, + "quantity_string": "1", + "timestamp": "2023-12-11T15:39:00", + "block_number": 820722, + "transaction": "", + "transaction_initiator": null + }, + "rarity": { + "rank": null, + "score": null, + "unique_attributes": null + }, + "royalty": [], + "extra_metadata": { + "attributes": [], + "utxo_details": { + "distinct_rare_sats": 1, + "satributes": { + "block_9": { + "count": 1, + "display_name": "Block 9", + "description": "Sats mined in block 9 which are the oldest sats in circulation", + "icon": "https://cdn.simplehash.com/rare_sats/satribute_block9.png" + }, + "first_tx": { + "count": 1, + "display_name": "First Transaction", + "description": "Sat from the 10 Bitcoins sent from Satashi Nakamoto to Hal Finney in the first Bitcoin transaction ever on January 12, 2009", + "icon": "https://cdn.simplehash.com/rare_sats/satribute_first_transaction.png" + }, + "nakamoto": { + "count": 1, + "display_name": "Nakamoto", + "description": "Sat mined by Satoshi Nakamoto", + "icon": "https://cdn.simplehash.com/rare_sats/satribute_nakamoto.png" + }, + "vintage": { + "count": 1, + "display_name": "Vintage", + "description": "Sats mined in the first 1,000 blocks", + "icon": "https://cdn.simplehash.com/rare_sats/satribute_vintage.png" + }, + "common": { + "count": 330, + "display_name": "Common", + "description": "Any sat that is not the first sat of its block", + "icon": "https://cdn.simplehash.com/rare_sats/satribute_common.png" + } + }, + "sat_ranges": [ + { + "starting_sat": 45558858287, + "value": 1, + "distinct_rare_sats": 1, + "year": "2009", + "subranges": [ + { + "starting_sat": 45558858287, + "value": 1, + "sat_types": ["block_9", "first_tx", "nakamoto", "vintage"] + } + ] + }, + { + "starting_sat": 640648318319738, + "value": 330, + "distinct_rare_sats": 0, + "year": "2011", + "subranges": [ + { + "starting_sat": 640648318319738, + "value": 330, + "sat_types": ["common"] + } + ] + } + ], + "block_number": 820722, + "value": 331, + "script_pub_key": { + "asm": "", + "desc": "", + "hex": "", + "address": "", + "type": "" + } + }, + "image_original_url": null, + "animation_original_url": null, + "metadata_original_url": null + } + }, + { + "nft_id": "utxo.5e9b99b09f1f259bf2d0afda1ee819abca2de094f8c58c56dd5a5b754f766204.1", + "chain": "utxo", + "contract_address": "5e9b99b09f1f259bf2d0afda1ee819abca2de094f8c58c56dd5a5b754f766204", + "token_id": "1", + "name": null, + "description": null, + "previews": { + "image_small_url": null, + "image_medium_url": null, + "image_large_url": null, + "image_opengraph_url": null, + "blurhash": null, + "predominant_color": null + }, + "image_url": null, + "image_properties": null, + "video_url": null, + "video_properties": null, + "audio_url": null, + "audio_properties": null, + "model_url": null, + "model_properties": null, + "other_url": null, + "other_properties": null, + "background_color": null, + "external_url": null, + "created_date": "2023-12-11T15:00:05", + "status": "minted", + "token_count": 1, + "owner_count": 1, + "owners": [ + { + "owerner_address": "weneverknow", + "quantity": 1, + "quantity_string": "1", + "first_acquired_date": "2023-12-11T15:00:05", + "last_acquired_date": "2023-12-11T15:00:05" + } + ], + "contract": { + "type": "UTXO", + "name": null, + "symbol": null, + "deployed_by": null, + "deployed_via_contract": null, + "owned_by": null, + "has_multiple_collections": false + }, + "collection": { + "collection_id": null, + "name": null, + "description": null, + "image_url": null, + "image_properties": null, + "banner_image_url": null, + "category": null, + "is_nsfw": null, + "external_url": null, + "twitter_username": null, + "discord_url": null, + "instagram_username": null, + "medium_username": null, + "telegram_url": null, + "marketplace_pages": [], + "metaplex_mint": null, + "metaplex_candy_machine": null, + "metaplex_first_verified_creator": null, + "spam_score": null, + "floor_prices": [], + "top_bids": [], + "distinct_owner_count": null, + "distinct_nft_count": null, + "total_quantity": null, + "chains": [], + "top_contracts": [], + "collection_royalties": [] + }, + "last_sale": null, + "primary_sale": null, + "first_created": { + "minted_to": "bc1pgtat0n2kavrz4ufhngm2muzxzx6pcmvr4czp089v48u5sgvpd9vqjsuaql", + "quantity": 1, + "quantity_string": "1", + "timestamp": "2023-12-11T15:00:05", + "block_number": 820718, + "transaction": "", + "transaction_initiator": null + }, + "rarity": { + "rank": null, + "score": null, + "unique_attributes": null + }, + "royalty": [], + "extra_metadata": { + "attributes": [], + "utxo_details": { + "distinct_rare_sats": 0, + "satributes": { + "common": { + "count": 10000, + "display_name": "Common", + "description": "Any sat that is not the first sat of its block", + "icon": "https://cdn.simplehash.com/rare_sats/satribute_common.png" + } + }, + "sat_ranges": [ + { + "starting_sat": 926537729411206, + "value": 546, + "distinct_rare_sats": 0, + "year": "2012", + "subranges": [ + { + "starting_sat": 926537729411206, + "value": 546, + "sat_types": ["common"] + } + ] + }, + { + "starting_sat": 1612575222953082, + "value": 9454, + "distinct_rare_sats": 0, + "year": "2017", + "subranges": [ + { + "starting_sat": 1612575222953082, + "value": 9454, + "sat_types": ["common"] + } + ] + } + ], + "block_number": 820718, + "value": 10000, + "script_pub_key": { + "asm": "", + "desc": "", + "hex": "", + "address": "", + "type": "" + } + }, + "image_original_url": null, + "animation_original_url": null, + "metadata_original_url": null + } + }, + { + "nft_id": "bitcoin.e5757a74ba8121f7f81c0c568371e29d42235943a0f0c60ce7a59d9b104e7d67i0", + "chain": "bitcoin", + "contract_address": "e5757a74ba8121f7f81c0c568371e29d42235943a0f0c60ce7a59d9b104e7d67i0", + "token_id": null, + "name": "Bitcoin Flower", + "description": null, + "previews": { + "image_small_url": "https://lh3.googleusercontent.com/l1i9mrMjxrIs57l2EfawBJde67CYdNQFojn8dC2jyJSIW73J0pR4uv7GP9CCbGvgalsLWyI-KD1xNgfaVPFIGiYX71oMxqq9kx8=s250", + "image_medium_url": "https://lh3.googleusercontent.com/l1i9mrMjxrIs57l2EfawBJde67CYdNQFojn8dC2jyJSIW73J0pR4uv7GP9CCbGvgalsLWyI-KD1xNgfaVPFIGiYX71oMxqq9kx8", + "image_large_url": "https://lh3.googleusercontent.com/l1i9mrMjxrIs57l2EfawBJde67CYdNQFojn8dC2jyJSIW73J0pR4uv7GP9CCbGvgalsLWyI-KD1xNgfaVPFIGiYX71oMxqq9kx8=s1000", + "image_opengraph_url": "https://lh3.googleusercontent.com/l1i9mrMjxrIs57l2EfawBJde67CYdNQFojn8dC2jyJSIW73J0pR4uv7GP9CCbGvgalsLWyI-KD1xNgfaVPFIGiYX71oMxqq9kx8=k-w1200-s2400-rj", + "blurhash": "U6JGg5WC}[$*#JWUO$sq-Tj?9[WUTBjunBj[", + "predominant_color": "#ab8483" + }, + "image_url": "https://cdn.simplehash.com/assets/c5def939b99b6eacd018b7bf25ffdd52dd228a7aef0770796306251c84f69460.webp", + "image_properties": { + "width": 48, + "height": 48, + "size": 362, + "mime_type": "image/webp", + "exif_orientation": null + }, + "video_url": null, + "video_properties": null, + "audio_url": null, + "audio_properties": null, + "model_url": null, + "model_properties": null, + "other_url": null, + "other_properties": null, + "background_color": null, + "external_url": null, + "created_date": "2023-06-11T00:40:10", + "status": "minted", + "token_count": 1, + "owner_count": 1, + "owners": [ + { + "owerner_address": "weneverknow", + "quantity": 1, + "quantity_string": "1", + "first_acquired_date": "2023-12-11T15:00:05", + "last_acquired_date": "2023-12-11T15:00:05" + } + ], + "contract": { + "type": "ORDINALS", + "name": "Inscription #11382871", + "symbol": null, + "deployed_by": null, + "deployed_via_contract": null, + "owned_by": null, + "has_multiple_collections": false + }, + "collection": { + "collection_id": "a7a586797ba73659efcbd802e35a2cf3", + "name": "Bitcoin Flowers", + "description": "Bitcoin Flowers are an integrated part of the Bitcoin Bees ecosystem.Collect the flowers and make your own taproot garden, the Bees will love it..", + "image_url": "https://lh3.googleusercontent.com/iNZ7hNXb9e5efu4kcTuTz2bjyo3xMflPJLbTW_q45HDBB3KJ2D__VuVMf0FMT1aFwb1VtaEloOpecw-QZqwbJmlgLSWG3liFuuk", + "image_properties": { + "width": 500, + "height": 500, + "mime_type": "image/png" + }, + "banner_image_url": null, + "category": null, + "is_nsfw": null, + "external_url": null, + "twitter_username": "OrdinalBees", + "discord_url": "https://discord.gg/9aX49Kbc5A", + "instagram_username": null, + "medium_username": null, + "telegram_url": null, + "marketplace_pages": [ + { + "marketplace_id": "magiceden", + "marketplace_name": "Magic Eden", + "marketplace_collection_id": "bitcoin-flowers", + "nft_url": "https://magiceden.io/ordinals/item-details/e5757a74ba8121f7f81c0c568371e29d42235943a0f0c60ce7a59d9b104e7d67i0", + "collection_url": "https://magiceden.io/ordinals/marketplace/bitcoin-flowers", + "verified": null + } + ], + "metaplex_mint": null, + "metaplex_candy_machine": null, + "metaplex_first_verified_creator": null, + "spam_score": 0, + "floor_prices": [ + { + "marketplace_id": "magiceden", + "marketplace_name": "Magic Eden", + "value": 8800, + "payment_token": { + "payment_token_id": "bitcoin.native", + "name": "Bitcoin", + "symbol": "BTC", + "address": null, + "decimals": 8 + }, + "value_usd_cents": 512 + }, + { + "marketplace_id": "okx", + "marketplace_name": "OKX", + "value": 17000, + "payment_token": { + "payment_token_id": "bitcoin.native", + "name": "Bitcoin", + "symbol": "BTC", + "address": null, + "decimals": 8 + }, + "value_usd_cents": 989 + }, + { + "marketplace_id": "unisat", + "marketplace_name": "UniSat", + "value": 50000, + "payment_token": { + "payment_token_id": "bitcoin.native", + "name": "Bitcoin", + "symbol": "BTC", + "address": null, + "decimals": 8 + }, + "value_usd_cents": 2909 + } + ], + "top_bids": [], + "distinct_owner_count": 2922, + "distinct_nft_count": 10000, + "total_quantity": 10000, + "chains": ["bitcoin"], + "top_contracts": [], + "collection_royalties": [] + }, + "last_sale": { + "from_address": "bc1p8dd094pp8gepf5kp82qgkgw4hq06yly29u7dr7g0xwn4k0muudgqljurum", + "to_address": "bc1pgtat0n2kavrz4ufhngm2muzxzx6pcmvr4czp089v48u5sgvpd9vqjsuaql", + "quantity": 1, + "quantity_string": "1", + "timestamp": "2023-12-11T15:00:05", + "transaction": "", + "marketplace_id": "magiceden", + "marketplace_name": "Magic Eden", + "is_bundle_sale": false, + "payment_token": { + "payment_token_id": "bitcoin.native", + "name": "Bitcoin", + "symbol": "BTC", + "address": null, + "decimals": 8 + }, + "unit_price": 8500, + "total_price": 8500, + "unit_price_usd_cents": 356 + }, + "primary_sale": null, + "first_created": { + "minted_to": "bc1pwfvcyx75cxahelslz8jk5awc8hkptdgwlq2zu9g804q48h44h3pss2v5rn", + "quantity": 1, + "quantity_string": "1", + "timestamp": "2023-06-11T00:40:10", + "block_number": 793795, + "transaction": "", + "transaction_initiator": null + }, + "rarity": { + "rank": null, + "score": null, + "unique_attributes": null + }, + "royalty": [], + "extra_metadata": { + "attributes": [], + "ordinal_details": { + "inscription_id": "e5757a74ba8121f7f81c0c568371e29d42235943a0f0c60ce7a59d9b104e7d67i0", + "inscription_number": 11382871, + "content_length": 362, + "content_type": "image/webp", + "sat_number": 926537729411206, + "sat_name": "hhcguwtfjlz", + "sat_rarity": "common", + "protocol_name": null, + "protocol_content": null, + "location": "5e9b99b09f1f259bf2d0afda1ee819abca2de094f8c58c56dd5a5b754f766204:1:0", + "output_value": 10000, + "parents": null, + "charms": null + }, + "image_original_url": "https://ordinals.simplehash.com/content/e5757a74ba8121f7f81c0c568371e29d42235943a0f0c60ce7a59d9b104e7d67i0", + "animation_original_url": null, + "metadata_original_url": null + } + }, + { + "nft_id": "bitcoin.5ef46a44ee9777f77670940cadd760df3be6583b4724d50a923bde0d19317894i0", + "chain": "bitcoin", + "contract_address": "5ef46a44ee9777f77670940cadd760df3be6583b4724d50a923bde0d19317894i0", + "token_id": null, + "name": "Timechain #136", + "description": null, + "previews": { + "image_small_url": "https://lh3.googleusercontent.com/bEJ7jNgQqze5h93mq_EpSVQKr6YLOpQDEjn013rXaH9bAiQvvHMV9FiLh4I6oo9_OydqImDDULqT0PExTGappBo2Mvr6oBDJZ3Me=s250", + "image_medium_url": "https://lh3.googleusercontent.com/bEJ7jNgQqze5h93mq_EpSVQKr6YLOpQDEjn013rXaH9bAiQvvHMV9FiLh4I6oo9_OydqImDDULqT0PExTGappBo2Mvr6oBDJZ3Me", + "image_large_url": "https://lh3.googleusercontent.com/bEJ7jNgQqze5h93mq_EpSVQKr6YLOpQDEjn013rXaH9bAiQvvHMV9FiLh4I6oo9_OydqImDDULqT0PExTGappBo2Mvr6oBDJZ3Me=s1000", + "image_opengraph_url": "https://lh3.googleusercontent.com/bEJ7jNgQqze5h93mq_EpSVQKr6YLOpQDEjn013rXaH9bAiQvvHMV9FiLh4I6oo9_OydqImDDULqT0PExTGappBo2Mvr6oBDJZ3Me=k-w1200-s2400-rj", + "blurhash": "U14Cw{j[0wfjD%a|-pay5OfQ=}j[=}jt55fj", + "predominant_color": "#13352c" + }, + "image_url": "https://cdn.simplehash.com/assets/ffc566633c3bd957c4f7325b48e16a05b9fa9be93f457966e150c04459f4784c.png", + "image_properties": { + "width": 960, + "height": 960, + "size": 2417953, + "mime_type": "image/png", + "exif_orientation": null + }, + "video_url": null, + "video_properties": null, + "audio_url": null, + "audio_properties": null, + "model_url": null, + "model_properties": null, + "other_url": "https://ordinals.simplehash.com/content/5ef46a44ee9777f77670940cadd760df3be6583b4724d50a923bde0d19317894i0", + "other_properties": { + "mime_type": "text/html" + }, + "background_color": null, + "external_url": null, + "created_date": "2023-02-13T21:12:28", + "status": "minted", + "token_count": 1, + "owner_count": 1, + "owners": [ + { + "owner_address": "bc1puq6wydt80qtc9kejhn3qjd0nzkwfplqgfwkmctl55npuwrskuu3slrnnj3", + "quantity": 1, + "quantity_string": "1", + "first_acquired_date": "2023-02-13T21:29:06", + "last_acquired_date": "2023-02-13T21:29:06" + } + ], + "contract": { + "type": "ORDINALS", + "name": "Inscription #76557", + "symbol": null, + "deployed_by": null, + "deployed_via_contract": null, + "owned_by": null, + "has_multiple_collections": false + }, + "collection": { + "collection_id": "0b8bd940186a1d33bba650287fa0e0d7", + "name": "Timechain", + "description": "Timechain is the first long-form generative art collection on Bitcoin. It was created to honor Satoshi Nakamoto’s original vision for Bitcoin (“the timechain”) and celebrate the launch of Ordinals.Timechain is living artwork. An animated, ever-changing hourglass serves as the focal point. A kaleidoscope of colors that allows you to visualize the two opposite sides of time: continuous and fractured.Like water flowing down a river, time is continuous. Yet we measure time in fractions: seconds, minutes, and hours. The sand falling through the hourglass is fluid and continuous, but the shadow of the hourglass remains still.Complementing the hourglass in the background are sine waves that dance in an orderly tandem with the hourglass, creating a hypnotic visual experience. But at special times — like angel number moments — the artwork bursts into a polychromatic dance, with the background waves moving in chaotic directions, representing an intersection between the dimensions of time.", + "image_url": "https://lh3.googleusercontent.com/3-Tx0wtd71-8PvpYhYoUzAz4ex6lBgclifDSAdzjeK_5W5GMmjYztDrS11-TZ0GhoNLFFVYt0iWTJhdBrP32L_HVFFrQ3AMdvQ", + "image_properties": { + "width": 512, + "height": 460, + "mime_type": "image/png" + }, + "banner_image_url": null, + "category": null, + "is_nsfw": null, + "external_url": "https://generative.xyz/generative/1000001", + "twitter_username": "NewBitcoinCity", + "discord_url": "https://discord.gg/yNbatuGMDG", + "instagram_username": null, + "medium_username": null, + "telegram_url": null, + "marketplace_pages": [ + { + "marketplace_id": "magiceden", + "marketplace_name": "Magic Eden", + "marketplace_collection_id": "timechain", + "nft_url": "https://magiceden.io/ordinals/item-details/5ef46a44ee9777f77670940cadd760df3be6583b4724d50a923bde0d19317894i0", + "collection_url": "https://magiceden.io/ordinals/marketplace/timechain", + "verified": null + } + ], + "metaplex_mint": null, + "metaplex_candy_machine": null, + "metaplex_first_verified_creator": null, + "spam_score": 0, + "floor_prices": [ + { + "marketplace_id": "magiceden", + "marketplace_name": "Magic Eden", + "value": 1100000, + "payment_token": { + "payment_token_id": "bitcoin.native", + "name": "Bitcoin", + "symbol": "BTC", + "address": null, + "decimals": 8 + }, + "value_usd_cents": 63233 + } + ], + "top_bids": [], + "distinct_owner_count": 105, + "distinct_nft_count": 140, + "total_quantity": 140, + "chains": ["bitcoin"], + "top_contracts": [], + "collection_royalties": [] + }, + "last_sale": null, + "primary_sale": null, + "first_created": { + "minted_to": "bc1ppz5f0u8k8zkjptx56flxx2ul9pa29wgr9aql2yk5rpwusfpkh5cq2yau2p", + "quantity": 1, + "quantity_string": "1", + "timestamp": "2023-02-13T21:12:28", + "block_number": 776398, + "transaction": "", + "transaction_initiator": null + }, + "rarity": { + "rank": null, + "score": null, + "unique_attributes": null + }, + "royalty": [], + "extra_metadata": { + "attributes": [], + "high_res_img_url": "https://cdn.generative.xyz/thumb/5ef46a44ee9777f77670940cadd760df3be6583b4724d50a923bde0d19317894i0-1677063161.png", + "ordinal_details": { + "inscription_id": "5ef46a44ee9777f77670940cadd760df3be6583b4724d50a923bde0d19317894i0", + "inscription_number": 76557, + "content_length": 104383, + "content_type": "text/html;charset=utf-8", + "sat_number": 741545957501281, + "sat_name": "ipedezionvs", + "sat_rarity": "common", + "protocol_name": null, + "protocol_content": null, + "location": "71f53e77b75ae4ae4f56ad147553e1586fb0eab5ae69ac676cfb644954e46606:0:0", + "output_value": 8335 + }, + "image_original_url": "https://ordinals.simplehash.com/content/5ef46a44ee9777f77670940cadd760df3be6583b4724d50a923bde0d19317894i0", + "animation_original_url": null, + "metadata_original_url": null + } + } + ] +} diff --git a/apps/ledger-live-desktop/tests/handlers/nfts.ts b/apps/ledger-live-desktop/tests/handlers/nfts.ts index 9835c596e608..c8cda883236c 100644 --- a/apps/ledger-live-desktop/tests/handlers/nfts.ts +++ b/apps/ledger-live-desktop/tests/handlers/nfts.ts @@ -1,10 +1,11 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import { http, HttpResponse } from "msw"; -import { mockedResponse } from "./fixtures/nfts"; +import { ETHmockedResponse } from "./fixtures/nfts"; +import { OrdinalsMockedResponse } from "./fixtures/ordinals"; const handlers = [ http.post("https://nft.api.live.ledger.com/v1/ethereum/1/contracts/tokens/infos", () => { - 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/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; };