diff --git a/src/app/components/rareSatAsset/rareSatAsset.tsx b/src/app/components/rareSatAsset/rareSatAsset.tsx index 2915738ae..62e05b485 100644 --- a/src/app/components/rareSatAsset/rareSatAsset.tsx +++ b/src/app/components/rareSatAsset/rareSatAsset.tsx @@ -55,7 +55,7 @@ function RareSatAsset({ item, isCollage = false }: Props) { {isInscription ? ( - {!isCollage && !!item.rarity_ranking && item.rarity_ranking !== 'common' && ( + {!isCollage && !!item.rarity_ranking && item.rarity_ranking !== 'COMMON' && ( 360; const src = { - epic: Epic, - legendary: Legendary, - mythic: Mythic, - rare: Rare, - uncommon: Uncommon, - unknown: Unknown, + EPIC: Epic, + LEGENDARY: Legendary, + MYTHIC: Mythic, + RARE: Rare, + UNCOMMON: Uncommon, + UNKNOWN: Unknown, BLACK_LEGENDARY: BlackLegendary, BLACK_EPIC: BlackEpic, BLACK_RARE: BlackRare, @@ -136,7 +136,7 @@ function RareSatIcon({ return ( - {glow && type !== 'unknown' && ( + {glow && type !== 'UNKNOWN' && ( )} {type} diff --git a/src/app/hooks/queries/ordinals/tempAddressRareSatsMock.ts b/src/app/hooks/queries/ordinals/tempAddressRareSatsMock.ts new file mode 100644 index 000000000..13dba0e25 --- /dev/null +++ b/src/app/hooks/queries/ordinals/tempAddressRareSatsMock.ts @@ -0,0 +1,280 @@ +import { ApiBundleV2 } from '@utils/rareSats'; + +export type Response = { + xVersion: number; + limit: number; + offset: number; + total: number; + results: ApiBundleV2[]; +}; + +export const mockData: Response = { + xVersion: 1, + limit: 30, + offset: 0, + total: 1, + results: [ + { + block_height: 803128, + txid: 'f5aa0649f2e5d0c6402c2d6b64ba6ea89e8be836a2ad01cbb0cdcc8721e314c7', + value: 21000, + vout: 0, + sat_ranges: [ + { + year_mined: 2009, + block: 9, + offset: 0, + range: { + start: '34234320000000', + end: '34234320010000', + }, + satributes: ['UNCOMMON', 'PIZZA', 'PALINDROME'], + inscriptions: [], + }, + { + year_mined: 2009, + block: 9, + offset: 10000, + range: { + start: '34234320010001', + end: '34234320010002', + }, + satributes: ['UNCOMMON', 'PIZZA', 'PALINDROME'], + inscriptions: [ + { + content_type: 'text/plain;charset=utf-8', + id: '09c094c3f1ab71c9be7a356a0f1af21b0e552c18d50372fd3799430c890ef135i0', + }, + ], + }, + { + year_mined: 2009, + block: 9, + offset: 10002, + range: { + start: '34234320010002', + end: '34234320020000', + }, + satributes: ['UNCOMMON', 'PIZZA', 'PALINDROME'], + inscriptions: [], + }, + { + year_mined: 2009, + block: 9, + offset: 20001, + range: { + start: '45000000001', + end: '45000000999', + }, + satributes: ['BLOCK9', 'NAKAMOTO', 'VINTAGE', 'FIRST_TRANSACTION'], + inscriptions: [], + }, + ], + }, + ], +}; + +// TestCase 1 - Empty response +export const mockTestCase1: Response = { + xVersion: 1, + limit: 30, + offset: 0, + total: 0, + results: [], +}; + +// TestCase 2 - 3 bundles +export const mockTestCase3: Response = { + xVersion: 1, + limit: 30, + offset: 0, + total: 1, + results: [ + { + block_height: 803128, + txid: 'f5aa0649f2e5d0c6402c2d6b64ba6ea89e8be836a2ad01cbb0cdcc8721e314c7', + value: 20997, + vout: 0, + sat_ranges: [ + { + year_mined: 2009, + block: 9, + offset: 0, + range: { + start: '34234320000000', + end: '34234320010000', + }, + satributes: ['RARE', 'PIZZA', 'PALINDROME'], + inscriptions: [], + }, + { + year_mined: 2009, + block: 9, + offset: 10000, + range: { + start: '34234320010001', + end: '34234320010002', + }, + satributes: ['UNCOMMON', 'PIZZA', 'PALINDROME'], + inscriptions: [ + { + content_type: 'text/plain;charset=utf-8', + id: '09c094c3f1ab71c9be7a356a0f1af21b0e552c18d50372fd3799430c890ef135i0', + }, + ], + }, + { + year_mined: 2009, + block: 9, + offset: 10002, + range: { + start: '34234320010002', + end: '34234320020000', + }, + satributes: ['UNCOMMON', 'PIZZA', 'PALINDROME'], + inscriptions: [], + }, + { + year_mined: 2009, + block: 9, + offset: 20001, + range: { + start: '45000000001', + end: '45000000999', + }, + satributes: ['BLOCK9', 'NAKAMOTO', 'VINTAGE', 'FIRST_TRANSACTION'], + inscriptions: [], + }, + ], + }, + { + block_height: 803128, + txid: 'f5aa0649f2e5d0c6402c2d6b64ba6ea89e8be836a2ad01cbb0cdcc8721e314c8', + value: 21000, + vout: 0, + sat_ranges: [ + { + year_mined: 2009, + block: 9, + offset: 0, + range: { + start: '34234320000000', + end: '34234320010000', + }, + satributes: ['BLOCK78'], + inscriptions: [], + }, + { + year_mined: 2009, + block: 9, + offset: 10000, + range: { + start: '34234320010001', + end: '34234320010001', + }, + satributes: ['PIZZA'], + inscriptions: [ + { + content_type: 'text/plain;charset=utf-8', + id: '09c094c3f1ab71c9be7a356a0f1af21b0e552c18d50372fd3799430c890ef135i0', + }, + ], + }, + { + year_mined: 2009, + block: 9, + offset: 10002, + range: { + start: '34234320010002', + end: '34234320020000', + }, + satributes: ['UNCOMMON', 'PIZZA', 'PALINDROME'], + inscriptions: [], + }, + { + year_mined: 2009, + block: 9, + offset: 20001, + range: { + start: '45000000001', + end: '45000000999', + }, + satributes: ['BLOCK9', 'NAKAMOTO'], + inscriptions: [], + }, + ], + }, + { + block_height: 803128, + txid: 'f5aa0649f2e5d0c6402c2d6b64ba6ea89e8be836a2ad01cbb0cdcc8721e314c9', + value: 21000, + vout: 0, + sat_ranges: [ + { + year_mined: 2009, + block: 9, + offset: 0, + range: { + start: '34234320000000', + end: '34234320000003', + }, + satributes: ['UNCOMMON', 'PIZZA', 'PALINDROME'], + inscriptions: [], + }, + { + year_mined: 2009, + block: 9, + offset: 3, + range: { + start: '34234320010001', + end: '34234320010001', + }, + satributes: ['UNCOMMON', 'PIZZA', 'PALINDROME'], + inscriptions: [ + { + content_type: 'text/plain;charset=utf-8', + id: '09c094c3f1ab71c9be7a356a0f1af21b0e552c18d50372fd3799430c890ef135i0', + }, + ], + }, + { + year_mined: 2009, + block: 9, + offset: 4, + range: { + start: '34234320010002', + end: '34234320010003', + }, + satributes: ['HITMAN', 'NAME_PALINDROME', 'NAKAMOTO', 'VINTAGE', 'FIRST_TRANSACTION'], + inscriptions: [], + }, + ], + }, + { + block_height: 803128, + txid: 'f5aa0649f2e5d0c6402c2d6b64ba6ea89e8be836a2ad01cbb0cdcc8721e314d1', + value: 100, + vout: 0, + sat_ranges: [ + { + year_mined: 2009, + block: 9, + offset: 0, + range: { + start: '34234320000000', + end: '34234320000003', + }, + satributes: ['UNCOMMON', 'PIZZA', 'PALINDROME'], + inscriptions: [], + }, + ], + }, + { + block_height: 803128, + txid: 'f5aa0649f2e5d0c6402c2d6b64ba6ea89e8be836a2ad01cbb0cdcc8721e314d2', + value: 100, + vout: 0, + sat_ranges: [], + }, + ], +}; diff --git a/src/app/hooks/queries/ordinals/useAddressRareSats.ts b/src/app/hooks/queries/ordinals/useAddressRareSats.ts index 547cd9cf6..f4b88fd20 100644 --- a/src/app/hooks/queries/ordinals/useAddressRareSats.ts +++ b/src/app/hooks/queries/ordinals/useAddressRareSats.ts @@ -1,11 +1,112 @@ import useWalletSelector from '@hooks/useWalletSelector'; -import { getAddressUtxoOrdinalBundles, getUtxoOrdinalBundle } from '@secretkeylabs/xverse-core'; +import { + getAddressUtxoOrdinalBundles, + getUtxoOrdinalBundle, + NetworkType, +} from '@secretkeylabs/xverse-core'; +import { XVERSE_API_BASE_URL } from '@secretkeylabs/xverse-core/constant'; import { useInfiniteQuery, useQuery } from '@tanstack/react-query'; import { handleRetries, InvalidParamsError } from '@utils/query'; import { mapRareSatsAPIResponseToRareSats } from '@utils/rareSats'; +import axios from 'axios'; +import { mockData, mockTestCase1, mockTestCase3, Response } from './tempAddressRareSatsMock'; const PAGE_SIZE = 30; +// TODO: move this to xverse-core +export const getAddressUtxoOrdinalBundlesV2 = async ( + network: NetworkType, + address: string, + offset: number, + limit: number, + options?: { + /** Filter out unconfirmed UTXOs */ + hideUnconfirmed?: boolean; + /** Filter out UTXOs that only have one or more inscriptions (and no rare sats) */ + hideInscriptionOnly?: boolean; + }, +) => { + const params: Record = { + offset, + limit, + }; + + if (options?.hideUnconfirmed) { + params.hideUnconfirmed = 'true'; + } + if (options?.hideInscriptionOnly) { + params.hideInscriptionOnly = 'true'; + } + + const response = await axios.get( + `${XVERSE_API_BASE_URL(network)}/v2/address/${address}/ordinal-utxo`, + { + params, + }, + ); + + return response.data; +}; + +export const useAddressRareSatsV2 = () => { + const { ordinalsAddress, network } = useWalletSelector(); + + const getRareSatsByAddress = async ({ pageParam = 0 }) => { + if (!ordinalsAddress) { + throw new InvalidParamsError('ordinalsAddress is required'); + } + + // custom ordinal address takes precedence over mocks + const customOrdinalAddress = localStorage.getItem('ordinalAddress'); + const useProdApi = localStorage.getItem('useProdApi'); + + if (!(useProdApi || customOrdinalAddress)) { + // EMPTY RESPONSE + const testcase1 = localStorage.getItem('testcase1'); + if (testcase1) { + return mockTestCase1; + } + + // ERROR RESPONSE + const testcase2 = localStorage.getItem('testcase2'); + if (testcase2) { + throw new Error('Error response from API'); + } + + // 1 BUNDLE with 4 sat ranges + const testcase3 = localStorage.getItem('testcase3'); + if (testcase3) { + return mockTestCase3; + } + + return mockData; + } + + const bundleResponse = await getAddressUtxoOrdinalBundlesV2( + network.type, + customOrdinalAddress ?? ordinalsAddress, + pageParam, + PAGE_SIZE, + { + hideUnconfirmed: true, + hideInscriptionOnly: true, + }, + ); + return bundleResponse; + }; + + return useInfiniteQuery(['rare-sats', ordinalsAddress], getRareSatsByAddress, { + retry: handleRetries, + getNextPageParam: (lastPage, allPages) => { + const currentLength = allPages.map((page) => page.results).flat().length; + if (currentLength < lastPage.total) { + return currentLength; + } + }, + staleTime: 1 * 60 * 1000, // 1 min + }); +}; + export const useAddressRareSats = () => { const { ordinalsAddress, network } = useWalletSelector(); diff --git a/src/app/screens/nftDashboard/collectiblesTabs.tsx b/src/app/screens/nftDashboard/collectiblesTabs.tsx index a1ab7d94c..3dacb3b15 100644 --- a/src/app/screens/nftDashboard/collectiblesTabs.tsx +++ b/src/app/screens/nftDashboard/collectiblesTabs.tsx @@ -1,7 +1,7 @@ import ActionButton from '@components/button'; import WrenchErrorMessage from '@components/wrenchErrorMessage'; import { StyledP, StyledTab, StyledTabList } from '@ui-library/common.styled'; -import { ApiBundle, Bundle, mapRareSatsAPIResponseToRareSats } from '@utils/rareSats'; +import { ApiBundleV2, BundleV2, mapRareSatsAPIResponseToRareSatsV2 } from '@utils/rareSats'; import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate, useSearchParams } from 'react-router-dom'; @@ -12,6 +12,9 @@ import { StyledBarLoader, TilesSkeletonLoader } from '../../components/tilesSkel import Notice from './notice'; import RareSatsTabGridItem from './rareSatsTabGridItem'; +const MAX_SATS_ITEMS_EXTENSION = 6; +const MAX_SATS_ITEMS_GALLERY = 20; + export const GridContainer = styled.div<{ isGalleryOpen: boolean; }>((props) => ({ @@ -24,6 +27,12 @@ export const GridContainer = styled.div<{ : 'repeat(auto-fill,minmax(150px,1fr))', })); +export const RareSatsTabContainer = styled.div<{ + isGalleryOpen: boolean; +}>((props) => ({ + marginTop: props.theme.space.l, +})); + const StyledTotalItems = styled(StyledP)` margin-top: ${(props) => props.theme.space.s}; `; @@ -219,17 +228,21 @@ export default function CollectiblesTabs({ {rareSatsQuery.isLoading ? ( ) : ( - + {!rareSatsQuery.error && !rareSatsQuery.isLoading && rareSatsQuery.data?.pages ?.map((page) => page?.results) .flat() - .map((utxo: ApiBundle) => mapRareSatsAPIResponseToRareSats(utxo)) - .map((bundle: Bundle) => ( - + .map((utxo: ApiBundleV2) => mapRareSatsAPIResponseToRareSatsV2(utxo)) + .map((bundle: BundleV2) => ( + ))} - + )} {rareSatsQuery.hasNextPage && ( diff --git a/src/app/screens/nftDashboard/index.tsx b/src/app/screens/nftDashboard/index.tsx index ccd04392e..60b19f3a4 100644 --- a/src/app/screens/nftDashboard/index.tsx +++ b/src/app/screens/nftDashboard/index.tsx @@ -5,7 +5,7 @@ import ShowOrdinalReceiveAlert from '@components/showOrdinalReceiveAlert'; import BottomTabBar from '@components/tabBar'; import WebGalleryButton from '@components/webGalleryButton'; import useAddressInscriptionCollections from '@hooks/queries/ordinals/useAddressInscriptionCollections'; -import { useAddressRareSats } from '@hooks/queries/ordinals/useAddressRareSats'; +import { useAddressRareSatsV2 } from '@hooks/queries/ordinals/useAddressRareSats'; import useStacksCollectibles from '@hooks/queries/useStacksCollectibles'; import useWalletSelector from '@hooks/useWalletSelector'; import { ArrowDown, Wrench } from '@phosphor-icons/react'; @@ -131,7 +131,7 @@ export type NftDashboardState = { isOrdinalReceiveAlertVisible: boolean; stacksNftsQuery: ReturnType; inscriptionsQuery: ReturnType; - rareSatsQuery: ReturnType; + rareSatsQuery: ReturnType; openInGalleryView: () => void; onReceiveModalOpen: () => void; onReceiveModalClose: () => void; @@ -162,7 +162,7 @@ const useNftDashboard = (): NftDashboardState => { const [isOrdinalReceiveAlertVisible, setIsOrdinalReceiveAlertVisible] = useState(false); const stacksNftsQuery = useStacksCollectibles(); const inscriptionsQuery = useAddressInscriptionCollections(); - const rareSatsQuery = useAddressRareSats(); + const rareSatsQuery = useAddressRareSatsV2(); const ordinalsLength = inscriptionsQuery.data?.pages?.[0]?.total ?? 0; const totalNfts = stacksNftsQuery.data?.pages?.[0]?.total ?? 0; diff --git a/src/app/screens/nftDashboard/inscriptionsTabGridItem.tsx b/src/app/screens/nftDashboard/inscriptionsTabGridItem.tsx index 91a273407..02b20811b 100644 --- a/src/app/screens/nftDashboard/inscriptionsTabGridItem.tsx +++ b/src/app/screens/nftDashboard/inscriptionsTabGridItem.tsx @@ -73,7 +73,7 @@ export function InscriptionsTabGridItem({ onClick={isCollection(collection) ? handleClickCollectionId : handleClickInscriptionId} > {!collection.thumbnail_inscriptions ? ( // eslint-disable-line no-nested-ternary - + ) : !isCollection(collection) || collection.thumbnail_inscriptions.length === 1 ? ( // eslint-disable-line no-nested-ternary props.theme.space.m}; +`; + +const IconsContainer = styled.div` + flex: 1; + display: flex; + flex-direction: row; + justify-content: flex-end; `; const StyledBundleId = styled(StyledP)` - text-align: left; text-wrap: nowrap; overflow: hidden; + text-overflow: ellipsis; width: 100%; `; -const StyledBundleSub = styled(StyledP)` - text-align: left; - text-overflow: ellipsis; +const InscriptionText = styled(StyledP)` text-wrap: nowrap; overflow: hidden; + text-overflow: ellipsis; width: 100%; + margin-left: 4px; + width: 80px; `; -const GridItemContainer = styled.button` - display: flex; - flex-direction: column; - background: transparent; - gap: ${(props) => props.theme.space.s}; +const StyledBundleSub = styled(StyledP)` + text-align: left; width: 100%; `; -function RareSatsTabGridItem({ bundle }: { bundle: Bundle }) { - const navigate = useNavigate(); - const { setSelectedSatBundleDetails, setSelectedSatBundleItemIndex } = useSatBundleDataReducer(); - const isMoreThanOneItem = bundle.items?.length > 1; +const TileText = styled(StyledP)` + text-align: center; + color: ${(props) => props.theme.colors.white_200}; + padding: 2px 3px; +`; + +const ItemContainer = styled.div` + display: flex; + align-items: center; + flex-direction: row; + flex: 1; + padding: ${(props) => props.theme.space.m}; + margin-bottom: ${(props) => props.theme.space.s}; + border-radius: ${(props) => props.theme.space.xs}; + background-color: ${(props) => props.theme.colors.elevation1}; + justify-content: space-between; +`; + +const Range = styled.div` + display: flex; + flex-direction: row; + border-radius: 6px; + border: 1px solid var(--white-800, rgba(255, 255, 255, 0.2)); + margin-left: 2px; + align-items: center; + padding: 1px; +`; + +const Row = styled.div` + display: flex; + flex-direction: row; + align-items: center; + flex-direction: row; +`; + +function RareSatsTabGridItem({ bundle, maxItems }: { bundle: BundleV2; maxItems: number }) { + // const navigate = useNavigate(); + // const { setSelectedSatBundleDetails, setSelectedSatBundleItemIndex } = useSatBundleDataReducer(); + + const { t } = useTranslation('translation', { keyPrefix: 'RARE_SATS' }); const handleOnClick = () => { - setSelectedSatBundleDetails(bundle); - if (isMoreThanOneItem) { - return navigate('/nft-dashboard/rare-sats-bundle'); - } + // TODO: handle UI changes before navigate to this screens + // const isMoreThanOneItem = bundle.satRanges.length > 1; + // setSelectedSatBundleDetails(bundle); + // if (isMoreThanOneItem) { + // return navigate('/nft-dashboard/rare-sats-bundle'); + // } + // setSelectedSatBundleItemIndex(0); + // navigate('/nft-dashboard/rare-sats-detail'); + }; - setSelectedSatBundleItemIndex(0); - navigate('/nft-dashboard/rare-sats-detail'); + const renderedIcons = () => { + let totalIconsDisplayed = 0; + let totalTilesDisplayed = 0; + + return bundle.satributes.map((sats, index) => { + if (totalIconsDisplayed > maxItems) { + return null; + } + + if (totalIconsDisplayed >= maxItems - 1) { + totalIconsDisplayed += 1; + return ( + + + +{bundle.satributes.length - totalTilesDisplayed} + + + ); + } + totalTilesDisplayed += 1; + return ( + + {sats.map((sattribute, indexSattributes) => { + totalIconsDisplayed += 1; + if (totalIconsDisplayed >= maxItems - 1) { + return null; + } + // eslint-disable-next-line react/no-array-index-key + return ; + })} + {totalIconsDisplayed > maxItems - 2 ? ( + + ) : null} + + ); + }); }; - const bundleId = getBundleId(bundle); - const bundleSubText = getBundleSubText(bundle); + const bundleId = getFormattedTxIdVoutFromBundle(bundle); return ( - - + {bundleId} - - {bundleSubText} - + ( + + {value} + + )} + /> + + {bundle.inscriptions.map((inscription) => ( + + ordinal + + {inscription.id} + + + ))} - + {renderedIcons()} + ); } export default RareSatsTabGridItem; diff --git a/src/app/utils/inscriptions.ts b/src/app/utils/inscriptions.ts index 2afc4b87c..76a48e7ab 100644 --- a/src/app/utils/inscriptions.ts +++ b/src/app/utils/inscriptions.ts @@ -51,5 +51,5 @@ export const mapCondensedInscriptionToBundleItem = ( ): BundleItem => ({ inscription, type: 'inscription', - rarity_ranking: 'common', // TODO eventually want to fetch this rarity and display it + rarity_ranking: 'COMMON', // TODO eventually want to fetch this rarity and display it }); diff --git a/src/app/utils/rareSats.ts b/src/app/utils/rareSats.ts index 0e883eef2..749530928 100644 --- a/src/app/utils/rareSats.ts +++ b/src/app/utils/rareSats.ts @@ -2,12 +2,12 @@ import { t } from 'i18next'; import { getTruncatedAddress } from './helper'; export const RoadArmorRareSats = [ - 'mythic', - 'legendary', - 'epic', - 'rare', - 'uncommon', - 'common', + 'MYTHIC', + 'LEGENDARY', + 'EPIC', + 'RARE', + 'UNCOMMON', + 'COMMON', ] as const; export type RoadArmorRareSatsType = (typeof RoadArmorRareSats)[number]; @@ -40,11 +40,10 @@ export const Sattributes = [ export type SattributesType = (typeof Sattributes)[number]; // TODO: remove unknown and unify with common -export const RareSats = [...RoadArmorRareSats, 'unknown', ...Sattributes] as const; +export const RareSats = [...RoadArmorRareSats, 'UNKNOWN', ...Sattributes] as const; export type RareSatsType = (typeof RareSats)[number]; -export const getRareSatsLabelByType = (type: RareSatsType) => - t(`RARE_SATS.RARITY_LABEL.${type.toUpperCase()}`); +export const getRareSatsLabelByType = (type: RareSatsType) => t(`RARE_SATS.RARITY_LABEL.${type}`); export type SatType = 'inscription' | 'rare-sat' | 'inscribed-sat' | 'unknown'; @@ -58,10 +57,10 @@ export const getBundleItemSubText = ({ ({ inscription: t('COMMON.INSCRIPTION'), 'rare-sat': t('RARE_SATS.SAT_TYPES.RARE_SAT', { - type: getRareSatsLabelByType(rareSatsType ?? 'unknown'), + type: getRareSatsLabelByType(rareSatsType ?? 'UNKNOWN'), }), 'inscribed-sat': t('RARE_SATS.SAT_TYPES.INSCRIBED_RARE_SAT', { - type: getRareSatsLabelByType(rareSatsType ?? 'unknown'), + type: getRareSatsLabelByType(rareSatsType ?? 'UNKNOWN'), }), unknown: t('RARE_SATS.SAT_TYPES.UNKNOWN_RARE_SAT'), }[satType]); @@ -115,7 +114,7 @@ type SatInscription = { content_type: string; }; -type Sat = { number: string; offset: number; rarity_ranking: RoadArmorRareSatsType }; +type Sat = { number: string; offset: number; rarity_ranking: Lowercase }; export type ApiBundle = { txid: string; @@ -151,7 +150,7 @@ export type BundleItem = } | { type: 'unknown'; - rarity_ranking: 'unknown'; + rarity_ranking: 'UNKNOWN'; }; export type Bundle = Omit & { @@ -168,7 +167,7 @@ export const mapRareSatsAPIResponseToRareSats = (apiBundles: ApiBundle): Bundle // unknown if (!apiBundles.sats.length && !apiBundles.inscriptions.length) { - return { ...generalBundleInfo, items: [{ type: 'unknown', rarity_ranking: 'unknown' }] }; + return { ...generalBundleInfo, items: [{ type: 'unknown', rarity_ranking: 'UNKNOWN' }] }; } // only rare sats @@ -177,7 +176,7 @@ export const mapRareSatsAPIResponseToRareSats = (apiBundles: ApiBundle): Bundle ...generalBundleInfo, items: apiBundles.sats.map((sat) => ({ type: 'rare-sat', - rarity_ranking: sat.rarity_ranking, + rarity_ranking: sat.rarity_ranking.toUpperCase() as RoadArmorRareSatsType, number: sat.number, })), }; @@ -200,7 +199,7 @@ export const mapRareSatsAPIResponseToRareSats = (apiBundles: ApiBundle): Bundle } items.push({ type: 'inscription', - rarity_ranking: 'common', + rarity_ranking: 'COMMON', inscription: { id: inscription.id, content_type: inscription.content_type, @@ -213,13 +212,13 @@ export const mapRareSatsAPIResponseToRareSats = (apiBundles: ApiBundle): Bundle if (!inscription) { return items.push({ type: 'rare-sat', - rarity_ranking: sat.rarity_ranking, + rarity_ranking: sat.rarity_ranking.toUpperCase() as RoadArmorRareSatsType, number: sat.number, }); } items.push({ type: 'inscribed-sat', - rarity_ranking: sat.rarity_ranking, + rarity_ranking: sat.rarity_ranking.toUpperCase() as RoadArmorRareSatsType, number: sat.number, inscription: { id: inscription.id, @@ -234,7 +233,7 @@ export const mapRareSatsAPIResponseToRareSats = (apiBundles: ApiBundle): Bundle }; }; -const getFormattedTxIdVoutFromBundle = (bundle: Bundle) => +export const getFormattedTxIdVoutFromBundle = (bundle: Bundle | BundleV2) => `${getTruncatedAddress(bundle.txid, 6)}:${bundle.vout}`; export const getBundleId = (bundle: Bundle): string => { @@ -268,3 +267,108 @@ export const getBundleItemId = (bundle: Bundle, index: number): string => { } return item.number; }; + +// TODO: once we define the layout changes for inscriptions and buy/sell confirmation screen we can remove old implementation and remove the v2 from here +type Inscription = { + id: string; + content_type: string; +}; + +type BundleSatRange = Omit & { + totalSats: number; + yearMined: number; +}; + +export type BundleV2 = Omit & { + satRanges: BundleSatRange[]; + inscriptions: Inscription[]; + satributes: RareSatsType[][]; +}; + +export type ApiBundleSatRange = { + range: { + start: string; + end: string; + }; + year_mined: number; + block: number; + offset: number; + satributes: RareSatsType[]; + inscriptions: Inscription[]; +}; + +export type ApiBundleV2 = { + txid: string; + vout: number; + block_height?: number; + value: number; + sat_ranges: ApiBundleSatRange[]; +}; + +export const mapRareSatsAPIResponseToRareSatsV2 = (apiBundle: ApiBundleV2): BundleV2 => { + const generalBundleInfo = { + txid: apiBundle.txid, + vout: apiBundle.vout, + block_height: apiBundle.block_height, + value: apiBundle.value, + }; + + const commonUnknownRange: BundleSatRange = { + range: { + start: '0', + end: '0', + }, + yearMined: 0, + block: 0, + offset: 0, + satributes: ['UNKNOWN'], + inscriptions: [], + totalSats: apiBundle.value, + }; + + // if bundle has and empty sat ranges, it means that it's a common/unknown bundle + if (!apiBundle.sat_ranges.length) { + return { + ...generalBundleInfo, + satRanges: [commonUnknownRange], + inscriptions: [], + satributes: [['UNKNOWN']], + }; + } + + const satRanges = apiBundle.sat_ranges.map((satRange) => { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { year_mined, ...satRangeProps } = satRange; + return { + ...satRangeProps, + totalSats: Number(BigInt(satRange.range.end) - BigInt(satRange.range.start)), + yearMined: year_mined, + }; + }); + + // we map this previous to a possible push of a common unknown range cause we don't need to show the icon rare sats tab. We only show it if the bundle is fully common/unknown + const inscriptions = satRanges.reduce( + (acc, curr) => [...acc, ...curr.inscriptions], + [] as BundleV2['satRanges'][0]['inscriptions'], + ); + const satributes = satRanges.reduce( + (acc, curr) => [...acc, curr.satributes], + [] as RareSatsType[][], + ); + + // if totalExotics doesn't match the value of the bundle, it means that the bundle is not fully exotic and we need to add a common unknown sat range more + const totalExotics = satRanges.reduce((acc, curr) => acc + curr.totalSats, 0); + if (totalExotics !== apiBundle.value) { + satRanges.push({ + ...commonUnknownRange, + totalSats: apiBundle.value - totalExotics, + }); + } + + return { + ...generalBundleInfo, + satRanges, + inscriptions, + satributes, + }; +}; diff --git a/src/assets/img/rareSats/ic_ordinal_small.svg b/src/assets/img/rareSats/ic_ordinal_small.svg new file mode 100644 index 000000000..cb9e95ff4 --- /dev/null +++ b/src/assets/img/rareSats/ic_ordinal_small.svg @@ -0,0 +1,6 @@ + + + + + +