diff --git a/package-lock.json b/package-lock.json index 1127f1f8d..ac35a50f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "@ledgerhq/hw-transport-webusb": "^6.27.13", "@phosphor-icons/react": "^2.0.10", "@react-spring/web": "^9.6.1", - "@secretkeylabs/xverse-core": "3.1.1", + "@secretkeylabs/xverse-core": "4.0.0", "@stacks/connect": "^6.10.2", "@stacks/encryption": "4.3.5", "@stacks/stacks-blockchain-api-types": "6.1.1", @@ -1727,9 +1727,9 @@ } }, "node_modules/@secretkeylabs/xverse-core": { - "version": "3.1.1", - "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/3.1.1/c21f1fb2f3efb5938adb8ce211c1fc1e5521de33", - "integrity": "sha512-HyRLLFW1zE69xJfZhupcVoqi6f4k38cJtATMtSsIh3c0gC5ROWDH/W/FIdnVZV0vQ5o8bZj2GQmmww6leYAtsQ==", + "version": "4.0.0", + "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/4.0.0/b2776f6bd4a6eb065b31eafd6ab198c486af7b8d", + "integrity": "sha512-u4XBHz8VYeQGFtpjxfOIMMcx6OAusQx02j/fAanwURHAbgeX8P90esY4s8rtAFcOV16dGRqA/HHEIusnt3d6+Q==", "license": "ISC", "dependencies": { "@bitcoinerlab/secp256k1": "^1.0.2", @@ -16046,9 +16046,9 @@ } }, "@secretkeylabs/xverse-core": { - "version": "3.1.1", - "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/3.1.1/c21f1fb2f3efb5938adb8ce211c1fc1e5521de33", - "integrity": "sha512-HyRLLFW1zE69xJfZhupcVoqi6f4k38cJtATMtSsIh3c0gC5ROWDH/W/FIdnVZV0vQ5o8bZj2GQmmww6leYAtsQ==", + "version": "4.0.0", + "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/4.0.0/b2776f6bd4a6eb065b31eafd6ab198c486af7b8d", + "integrity": "sha512-u4XBHz8VYeQGFtpjxfOIMMcx6OAusQx02j/fAanwURHAbgeX8P90esY4s8rtAFcOV16dGRqA/HHEIusnt3d6+Q==", "requires": { "@bitcoinerlab/secp256k1": "^1.0.2", "@noble/secp256k1": "^1.7.1", diff --git a/package.json b/package.json index 89110eee9..83a967996 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "@ledgerhq/hw-transport-webusb": "^6.27.13", "@phosphor-icons/react": "^2.0.10", "@react-spring/web": "^9.6.1", - "@secretkeylabs/xverse-core": "3.1.1", + "@secretkeylabs/xverse-core": "4.0.0", "@stacks/connect": "^6.10.2", "@stacks/encryption": "4.3.5", "@stacks/stacks-blockchain-api-types": "6.1.1", diff --git a/src/app/components/assetModal/index.tsx b/src/app/components/assetModal/index.tsx new file mode 100644 index 000000000..a0274acc9 --- /dev/null +++ b/src/app/components/assetModal/index.tsx @@ -0,0 +1,88 @@ +import Cross from '@assets/img/dashboard/X.svg'; +import { animated, useSpring } from '@react-spring/web'; +import OrdinalImage from '@screens/ordinals/ordinalImage'; +import { CondensedInscription, SatRangeInscription } from '@secretkeylabs/xverse-core'; +import styled from 'styled-components'; + +const TransparentButton = styled.button({ + background: 'transparent', + display: 'flex', + alignItems: 'center', + marginLeft: 10, +}); + +const CrossContainer = styled.div({ + display: 'flex', + marginTop: 10, + justifyContent: 'flex-end', + alignItems: 'flex-end', +}); + +const OrdinalOuterImageContainer = styled.div({ + justifyContent: 'center', + alignItems: 'center', + borderRadius: 2, + display: 'flex', + flexDirection: 'column', + flex: 1, +}); + +const OrdinalImageContainer = styled.div({ + width: '50%', +}); + +const OrdinalBackgroundContainer = styled(animated.div)({ + width: '100%', + height: '100%', + top: 0, + left: 0, + bottom: 0, + right: 0, + position: 'fixed', + zIndex: 10, + background: 'rgba(18, 21, 30, 0.8)', + backdropFilter: 'blur(16px)', + padding: 16, + display: 'flex', + flexDirection: 'column', +}); + +interface Props { + inscription: SatRangeInscription; + onClose: () => void; +} + +function AssetModal({ inscription, onClose }: Props) { + const consdensedInscription: CondensedInscription = { + ...inscription, + number: inscription.inscription_number, + }; + const styles = useSpring({ + from: { + opacity: 0, + y: 24, + }, + to: { + y: 0, + opacity: 1, + }, + delay: 100, + }); + + return ( + + + + cross + + + + + + + + + ); +} + +export default AssetModal; diff --git a/src/app/components/bundleAsset/bundleAsset.tsx b/src/app/components/bundleAsset/bundleAsset.tsx deleted file mode 100644 index 284534cba..000000000 --- a/src/app/components/bundleAsset/bundleAsset.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import RareSatAsset from '@components/rareSatAsset/rareSatAsset'; -import { Bundle } from '@utils/rareSats'; -import styled from 'styled-components'; - -import CollectibleCollage from '../collectibleCollage/collectibleCollage'; - -const ImageContainer = styled.div` - display: flex; - justify-content: center; - align-items: center; - width: 100%; - aspect-ratio: 1; - overflow: hidden; - border-radius: ${(props) => props.theme.radius(1)}px; -`; - -const IndividualAssetContainer = styled.div` - display: flex; - width: 100%; - background: ${(props) => props.theme.colors.elevation1}; -`; - -interface Props { - bundle: Bundle; -} - -function BundleAsset({ bundle }: Props) { - const isMoreThanOneItem = bundle.items?.length > 1; - return ( - - {isMoreThanOneItem ? ( - - ) : ( - - - - )} - - ); -} - -export default BundleAsset; diff --git a/src/app/components/collectibleCollage/collectibleCollage.tsx b/src/app/components/collectibleCollage/collectibleCollage.tsx index e15ee56c5..c875cda75 100644 --- a/src/app/components/collectibleCollage/collectibleCollage.tsx +++ b/src/app/components/collectibleCollage/collectibleCollage.tsx @@ -1,7 +1,6 @@ -import RareSatAsset from '@components/rareSatAsset/rareSatAsset'; import Nft from '@screens/nftDashboard/nft'; -import { NonFungibleToken } from '@secretkeylabs/xverse-core'; -import { BundleItem } from '@utils/rareSats'; +import OrdinalImage from '@screens/ordinals/ordinalImage'; +import { CondensedInscription, NonFungibleToken } from '@secretkeylabs/xverse-core'; import styled from 'styled-components'; const CollageContainer = styled.div` @@ -39,10 +38,11 @@ const RemainingAmountOfAssets = styled.div((props) => ({ }, })); -function CollectibleCollage({ items }: { items: Array }) { +function CollectibleCollage({ items }: { items: Array }) { const moreThanFourItems = items.length > 4; - const isBundleItem = (item: any): boolean => (item as BundleItem).rarity_ranking !== undefined; + const isStacksNft = (item: CondensedInscription | NonFungibleToken): boolean => + 'asset_identifier' in item; return ( {items.slice(0, 4).map((item, index) => ( @@ -53,10 +53,10 @@ function CollectibleCollage({ items }: { items: Array+{items.length - 4}

) : // Conditionally render RareSatAsset if item is a BundleItem otherwise render Nft - isBundleItem(item) ? ( - - ) : ( + isStacksNft(item) ? ( + ) : ( + )} ))} diff --git a/src/app/components/confirmBtcTransactionComponent/bundle.tsx b/src/app/components/confirmBtcTransactionComponent/bundle.tsx new file mode 100644 index 000000000..84b728b71 --- /dev/null +++ b/src/app/components/confirmBtcTransactionComponent/bundle.tsx @@ -0,0 +1,109 @@ +import BundleIcon from '@assets/img/rareSats/satBundle.svg'; +import AssetModal from '@components/assetModal'; +import { CaretDown } from '@phosphor-icons/react'; +import { Bundle, BundleSatRange, SatRangeInscription } from '@secretkeylabs/xverse-core'; +import { StyledP } from '@ui-library/common.styled'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import styled from 'styled-components'; +import Theme from 'theme'; +import BundleItem from './bundleItem'; + +interface BundleItemContainerProps { + addMargin: boolean; +} + +const BundleItemsContainer = styled.div` + margin-top: ${(props) => (props.addMargin ? props.theme.space.m : 0)}; +`; + +const SatsBundleContainer = styled.div` + display: flex; + flex-direction: column; + margin-bottom: ${(props) => props.theme.space.s}; + border-radius: ${(props) => props.theme.space.s}; + padding: ${(props) => props.theme.space.m}; + background-color: ${(props) => props.theme.colors.elevation1}; +`; + +const SatsBundleButton = styled.button` + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + background-color: ${(props) => props.theme.colors.elevation1}; +`; + +const Row = styled.div` + display: flex; + flex-direction: row; + align-items: center; +`; + +const BundleTitle = styled(StyledP)` + margin-left: ${(props) => props.theme.space.s}; +`; + +const BundleValue = styled(StyledP)` + margin-right: ${(props) => props.theme.space.xs}; +`; + +const Title = styled(StyledP)((props) => ({ + marginBottom: props.theme.space.xs, +})); + +function SatsBundle({ bundle, title }: { bundle: Bundle; title?: string }) { + const [showBundleDetail, setShowBundleDetail] = useState(false); + const [inscriptionToShow, setInscriptionToShow] = useState( + undefined, + ); + + const { t } = useTranslation('translation'); + + return ( + <> + {inscriptionToShow && ( + setInscriptionToShow(undefined)} + inscription={inscriptionToShow} + /> + )} + + {title && {title}} + setShowBundleDetail((prevState) => !prevState)} + > + + bundle + + {t('RARE_SATS.SATS_BUNDLE')} + + + + {`${bundle.totalExoticSats} ${t( + 'NFT_DASHBOARD_SCREEN.RARE_SATS', + )}`} + + + + + {showBundleDetail && + bundle.satRanges.map((item: BundleSatRange, index: number) => ( + + { + // show ordinal modal to show asset + setInscriptionToShow(inscription); + }} + showDivider={index !== bundle.satRanges.length - 1} + /> + + ))} + + + ); +} + +export default SatsBundle; diff --git a/src/app/components/confirmBtcTransactionComponent/bundleItem.tsx b/src/app/components/confirmBtcTransactionComponent/bundleItem.tsx new file mode 100644 index 000000000..009220d0b --- /dev/null +++ b/src/app/components/confirmBtcTransactionComponent/bundleItem.tsx @@ -0,0 +1,128 @@ +import OrdinalIcon from '@assets/img/rareSats/ic_ordinal_small.svg'; +import RareSatIcon from '@components/rareSatIcon/rareSatIcon'; +import { DotsThree, Eye } from '@phosphor-icons/react'; +import { BundleSatRange, SatRangeInscription } from '@secretkeylabs/xverse-core'; +import { StyledP } from '@ui-library/common.styled'; +import { getSatLabel } from '@utils/rareSats'; +import { NumericFormat } from 'react-number-format'; +import styled from 'styled-components'; +import Theme from 'theme'; + +const RangeContainer = styled.div``; + +const Range = styled.div` + display: flex; + border-radius: 6px; + border: 1px solid ${(props) => props.theme.colors.white_800}; + padding: 1px; + flex-wrap: wrap; + flex-direction: row; + align-items: center; +`; + +interface ComponentWithDividerProps { + showDivider: boolean; +} + +const Container = styled.div` + padding-top:${(props) => props.theme.space.s}; + padding-bottom:${(props) => props.theme.space.s}; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + border-bottom: ${(props) => + props.showDivider ? `1px solid ${props.theme.colors.white_900}` : 'transparent'}; + width: 100%; +}`; + +const Column = styled.div` + display: flex; + flex: 1; + flex-direction: column; + align-items: flex-end; +`; + +const SatsText = styled(StyledP)` + width: 100%; + text-align: right; +`; +const InscriptionRow = styled.button` + display: flex; + flex-direction: row; + align-items: center; + background-color: transparent; +`; +const InscriptionText = styled(StyledP)` + text-wrap: nowrap; + overflow: hidden; + text-overflow: ellipsis; + width: 100%; + margin-left: 4px; +`; + +const BundleText = styled(StyledP)` + text-align: right; + width: 100%; +`; + +function BundleItem({ + item, + ordinalEyePressed, + showDivider, +}: { + item: BundleSatRange; + ordinalEyePressed: (inscription: SatRangeInscription) => void; + showDivider?: boolean; +}) { + const renderedIcons = () => ( + + + {item.satributes.map((satribute, index) => { + if (index > 4) return null; + if (index === 4) { + return ; + } + return ; + })} + + + ); + + return ( + + {renderedIcons()} + + {getSatLabel(item.satributes)} + 1 ? ' Sats' : ' Sat'} + thousandSeparator + renderText={(value: string) => ( + + {value} + + )} + /> + {item.inscriptions.map((inscription) => ( + { + ordinalEyePressed(inscription); + }} + > + ordinal + + {inscription.inscription_number} + + + + ))} + + + ); +} + +export default BundleItem; diff --git a/src/app/components/confirmBtcTransactionComponent/index.tsx b/src/app/components/confirmBtcTransactionComponent/index.tsx index 27d998118..6cf5e32e4 100644 --- a/src/app/components/confirmBtcTransactionComponent/index.tsx +++ b/src/app/components/confirmBtcTransactionComponent/index.tsx @@ -3,13 +3,14 @@ import AssetIcon from '@assets/img/transactions/Assets.svg'; import ActionButton from '@components/button'; import InfoContainer from '@components/infoContainer'; import RecipientComponent from '@components/recipientComponent'; -import TopRow from '@components/topRow'; import TransactionSettingAlert from '@components/transactionSetting'; import TransferFeeView from '@components/transferFeeView'; +import useNftDataSelector from '@hooks/stores/useNftDataSelector'; import useOrdinalsByAddress from '@hooks/useOrdinalsByAddress'; import useSeedVault from '@hooks/useSeedVault'; import useWalletSelector from '@hooks/useWalletSelector'; import { + Bundle, ErrorCodes, getBtcFiatEquivalent, Recipient, @@ -30,14 +31,18 @@ import { useTranslation } from 'react-i18next'; import { NumericFormat } from 'react-number-format'; import styled from 'styled-components'; import TransactionDetailComponent from '../transactionDetailComponent'; +import SatsBundle from './bundle'; -const OuterContainer = styled.div` +interface MainContainerProps { + isGalleryOpen: boolean; +} + +const OuterContainer = styled.div` display: flex; flex-direction: column; - overflow-y: auto; - &::-webkit-scrollbar { - display: none; - } + flex: 1; + flex-grow: 1; + ...${(props) => (props.isGalleryOpen ? props.theme.scrollbar : {})}; `; const Container = styled.div((props) => ({ @@ -45,8 +50,6 @@ const Container = styled.div((props) => ({ flexDirection: 'column', flex: 1, marginTop: props.theme.spacing(11), - marginLeft: props.theme.spacing(8), - marginRight: props.theme.spacing(8), })); interface ButtonProps { @@ -57,8 +60,6 @@ const ButtonContainer = styled.div((props) => ({ display: 'flex', flexDirection: 'row', position: 'relative', - marginLeft: props.theme.spacing(8), - marginRight: props.theme.spacing(8), marginBottom: props.isBtcSendBrowserTx ? props.theme.spacing(20) : props.theme.spacing(5), })); @@ -102,13 +103,13 @@ const ErrorText = styled.h1((props) => ({ })); interface ReviewTransactionTitleProps { - isOridnalTx: boolean; + centerAligned: boolean; } const ReviewTransactionText = styled.h1((props) => ({ ...props.theme.typography.headline_s, color: props.theme.colors.white_0, marginBottom: props.theme.spacing(16), - textAlign: props.isOridnalTx ? 'center' : 'left', + textAlign: props.centerAligned ? 'center' : 'left', })); const CalloutContainer = styled.div((props) => ({ @@ -131,6 +132,8 @@ interface Props { isBtcSendBrowserTx?: boolean; currencyType?: CurrencyTypes; isPartOfBundle?: boolean; + ordinalBundle?: Bundle; + holdsRareSats?: boolean; currentFeeRate: BigNumber; setCurrentFee: (feeRate: BigNumber) => void; setCurrentFeeRate: (feeRate: BigNumber) => void; @@ -154,6 +157,8 @@ function ConfirmBtcTransactionComponent({ isBtcSendBrowserTx, isPartOfBundle, currencyType, + ordinalBundle, + holdsRareSats, currentFeeRate, setCurrentFee, setCurrentFeeRate, @@ -165,12 +170,15 @@ function ConfirmBtcTransactionComponent({ const isGalleryOpen: boolean = document.documentElement.clientWidth > 360; const [loading, setLoading] = useState(false); const { btcAddress, selectedAccount, network, btcFiatRate, feeMultipliers } = useWalletSelector(); + const { selectedSatBundle } = useNftDataSelector(); const { getSeed } = useSeedVault(); const [showFeeSettings, setShowFeeSettings] = useState(false); const [error, setError] = useState(''); const [signedTx, setSignedTx] = useState(signedTxHex); const [total, setTotal] = useState(new BigNumber(0)); const [showFeeWarning, setShowFeeWarning] = useState(false); + + const bundle = selectedSatBundle ?? ordinalBundle ?? undefined; const { isLoading, data, @@ -373,10 +381,7 @@ function ConfirmBtcTransactionComponent({ return ( <> - - {!isBtcSendBrowserTx && !isGalleryOpen && ( - - )} + {showFeeWarning && ( + {t('CONFIRM_TRANSACTION.REVIEW_TRANSACTION')} @@ -398,11 +403,18 @@ function ConfirmBtcTransactionComponent({ /> )} + {holdsRareSats && ( + + + + )} + + {bundle && } {ordinalTxUtxo ? ( props.theme.space.m}; +`; + +const IconsContainer = styled.div` + flex: 1; + display: flex; + flex-direction: row; + justify-content: flex-end; +`; + +const StyledBundleId = styled(StyledP)` + text-wrap: nowrap; + overflow: hidden; + text-overflow: ellipsis; + width: 100%; + text-align: left; +`; + +const InscriptionText = styled(StyledP)` + text-wrap: nowrap; + overflow: hidden; + text-overflow: ellipsis; + width: 100%; + margin-left: 4px; + text-align: left; +`; + +const StyledBundleSub = styled(StyledP)` + text-align: left; + width: 100%; +`; + +const ItemContainer = styled.div` + display: flex; + align-items: center; + flex-direction: row; + flex: 1; + padding: ${(props) => props.theme.space.m}; + border-radius: ${(props) => props.theme.space.xs}; + background-color: ${(props) => props.theme.colors.elevation1}; + justify-content: space-between; +`; + +const Row = styled.div` + display: flex; + flex-direction: row; + align-items: center; + flex-direction: row; +`; + +function ExoticSatsRow({ + title, + satAmount, + inscriptions, + icons, + showNumberOfInscriptions = false, +}: { + title: string; + satAmount: number; + inscriptions: SatRangeInscription[]; + showNumberOfInscriptions?: boolean; + icons: ReactNode; +}) { + const { t } = useTranslation('translation', { keyPrefix: 'COMMON' }); + return ( + + + + {title} + + ( + + {value} + + )} + /> + {showNumberOfInscriptions && inscriptions.length ? ( + + ordinal + + {inscriptions.length > 1 + ? `+${inscriptions.length}` + : inscriptions[0].inscription_number} + + + ) : ( + inscriptions.map((inscription) => ( + + ordinal + + {inscription.inscription_number} + + + )) + )} + + {icons} + + ); +} + +export default ExoticSatsRow; diff --git a/src/app/components/rareSatAsset/rareSatAsset.tsx b/src/app/components/rareSatAsset/rareSatAsset.tsx deleted file mode 100644 index 2915738ae..000000000 --- a/src/app/components/rareSatAsset/rareSatAsset.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import RareSatIcon from '@components/rareSatIcon/rareSatIcon'; -import OrdinalImage from '@screens/ordinals/ordinalImage'; -import { Inscription } from '@secretkeylabs/xverse-core'; -import { BundleItem } from '@utils/rareSats'; -import styled from 'styled-components'; - -const Container = styled.div` - width: 100%; - height: 100%; -`; - -const InscriptionContainer = styled.div` - width: 100%; - height: 100%; - position: relative; - border-radius: 8px; - overflow: hidden; -`; - -const RareSatIconContainer = styled.div<{ isGallery: boolean }>((props) => ({ - display: 'flex', - position: 'absolute', - zIndex: 1, - left: props.isGallery ? 20 : 8, - top: props.isGallery ? 20 : 8, -})); - -const RareSatsContainer = styled.div((props) => ({ - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - width: '100%', - aspectRatio: '1', - overflow: 'hidden', - position: 'relative', - backgroundColor: props.theme.colors.elevation1, - borderRadius: 8, -})); - -const DynamicSizeContainer = styled.div<{ isCollage: boolean }>((props) => ({ - width: props.isCollage ? '40%' : '50%', - height: props.isCollage ? '40%' : '50%', -})); - -interface Props { - item: BundleItem; - isCollage?: boolean; -} - -function RareSatAsset({ item, isCollage = false }: Props) { - const isGallery: boolean = document.documentElement.clientWidth > 360; - const isInscription = item.type === 'inscription' || item.type === 'inscribed-sat'; - - return ( - - {isInscription ? ( - - {!isCollage && !!item.rarity_ranking && item.rarity_ranking !== 'common' && ( - - - - )} - - - ) : ( - - - - - - )} - - ); -} - -export default RareSatAsset; diff --git a/src/app/components/rareSatIcon/rareSatIcon.tsx b/src/app/components/rareSatIcon/rareSatIcon.tsx index def1d41f5..39715b138 100644 --- a/src/app/components/rareSatIcon/rareSatIcon.tsx +++ b/src/app/components/rareSatIcon/rareSatIcon.tsx @@ -1,12 +1,33 @@ +import OneDPali from '@assets/img/nftDashboard/rareSats/1d_pali.svg'; +import TwoDPali from '@assets/img/nftDashboard/rareSats/2d_pali.svg'; +import ThreeDPali from '@assets/img/nftDashboard/rareSats/3d_pali.svg'; +import Alpha from '@assets/img/nftDashboard/rareSats/alpha.svg'; +import BlackEpic from '@assets/img/nftDashboard/rareSats/black_epic.svg'; +import BlackLegendary from '@assets/img/nftDashboard/rareSats/black_legendary.svg'; +import BlackRare from '@assets/img/nftDashboard/rareSats/black_rare.svg'; +import BlackUncommon from '@assets/img/nftDashboard/rareSats/black_uncommon.svg'; +import Block78 from '@assets/img/nftDashboard/rareSats/block_78.svg'; +import Block9 from '@assets/img/nftDashboard/rareSats/block_9.svg'; +import BlockPali from '@assets/img/nftDashboard/rareSats/block_pali.svg'; import Epic from '@assets/img/nftDashboard/rareSats/epic.svg'; +import FibonacciSequence from '@assets/img/nftDashboard/rareSats/fibonacci_sequence.svg'; +import FirstTransactionSilkroad from '@assets/img/nftDashboard/rareSats/first_transaction_silkroad.svg'; +import Hitman from '@assets/img/nftDashboard/rareSats/hitman.svg'; +import Jpeg from '@assets/img/nftDashboard/rareSats/jpeg.svg'; import Legendary from '@assets/img/nftDashboard/rareSats/legendary.svg'; import Mythic from '@assets/img/nftDashboard/rareSats/mythic.svg'; +import Nakamoto from '@assets/img/nftDashboard/rareSats/nakamoto.svg'; +import Omega from '@assets/img/nftDashboard/rareSats/omega.svg'; +import Palinception from '@assets/img/nftDashboard/rareSats/palinception.svg'; +import Palindrome from '@assets/img/nftDashboard/rareSats/palindrome.svg'; +import Pizza from '@assets/img/nftDashboard/rareSats/pizza.svg'; import Rare from '@assets/img/nftDashboard/rareSats/rare.svg'; +import SequencePali from '@assets/img/nftDashboard/rareSats/sequence_pali.svg'; import Uncommon from '@assets/img/nftDashboard/rareSats/uncommon.svg'; import Unknown from '@assets/img/nftDashboard/rareSats/unknown.svg'; -import { getRareSatsColorsByRareSatsType, RareSatsType } from '@utils/rareSats'; +import Vintage from '@assets/img/nftDashboard/rareSats/vintage.svg'; +import { RareSatsType } from '@secretkeylabs/xverse-core'; import styled from 'styled-components'; - import Theme from '../../../theme'; const Container = styled.div<{ bgColor: string; padding: number }>((props) => ({ @@ -27,28 +48,6 @@ const Image = styled.img` height: 100%; zindex: 2; `; -type GlowProps = { - color: string; - outerColor: string; - isCollage: boolean; - isGallery: boolean; -}; -const Glow = styled.div((props) => { - const boxShadow = { - 'extension-collage': `0 0 calc(5vw) calc(1.5vw) ${props.color}`, - 'extension-one-item': `0 0 calc(12vw) calc(2vw) ${props.color}`, - 'gallery-collage': `0 0 calc(3.5vw) calc(0.8vw) ${props.color}`, - 'gallery-one-item': `0 0 calc(7vw) calc(1.5vw) ${props.color}`, - }[`${props.isGallery ? 'gallery' : 'extension'}-${props.isCollage ? 'collage' : 'one-item'}`]; - return { - position: 'absolute', - zIndex: 1, - width: '10%', - height: '10%', - borderRadius: '100%', - boxShadow, - }; -}); interface Props { type: RareSatsType; @@ -56,36 +55,48 @@ interface Props { bgColor?: keyof (typeof Theme)['colors']['background']; padding?: number; isDynamicSize?: boolean; - isCollage?: boolean; - glow?: boolean; } -function RareSatIcon({ - type, - size = 24, - bgColor, - padding = 0, - isDynamicSize = false, - isCollage = false, - glow = true, -}: Props) { - const isGallery: boolean = document.documentElement.clientWidth > 360; +function RareSatIcon({ type, size = 24, bgColor, padding = 0, isDynamicSize = false }: Props) { const src = { - epic: Epic, - legendary: Legendary, - mythic: Mythic, - rare: Rare, - uncommon: Uncommon, - unknown: Unknown, + EPIC: Epic, + LEGENDARY: Legendary, + MYTHIC: Mythic, + RARE: Rare, + UNCOMMON: Uncommon, + COMMON: Unknown, + BLACK_LEGENDARY: BlackLegendary, + BLACK_EPIC: BlackEpic, + BLACK_RARE: BlackRare, + BLACK_UNCOMMON: BlackUncommon, + FIBONACCI: FibonacciSequence, + '1D_PALINDROME': OneDPali, + '2D_PALINDROME': TwoDPali, + '3D_PALINDROME': ThreeDPali, + SEQUENCE_PALINDROME: SequencePali, + PERFECT_PALINCEPTION: Palinception, + PALIBLOCK_PALINDROME: BlockPali, + PALINDROME: Palindrome, + NAME_PALINDROME: Palindrome, + ALPHA: Alpha, + OMEGA: Omega, + FIRST_TRANSACTION: FirstTransactionSilkroad, + BLOCK9: Block9, + BLOCK78: Block78, + NAKAMOTO: Nakamoto, + VINTAGE: Vintage, + PIZZA: Pizza, + JPEG: Jpeg, + HITMAN: Hitman, + SILK_ROAD: FirstTransactionSilkroad, }[type]; + if (!src) { + return null; + } const backgroundColor = bgColor ? Theme.colors.background[bgColor] : 'transparent'; - const { color, backgroundColor: outerColor } = getRareSatsColorsByRareSatsType(type); return ( - {glow && type !== 'unknown' && ( - - )} {type} diff --git a/src/app/components/recipientComponent/index.tsx b/src/app/components/recipientComponent/index.tsx index 483d6d118..7b4ef8585 100644 --- a/src/app/components/recipientComponent/index.tsx +++ b/src/app/components/recipientComponent/index.tsx @@ -27,7 +27,7 @@ const Container = styled.div((props) => ({ const RecipientTitleText = styled.p((props) => ({ ...props.theme.body_medium_m, color: props.theme.colors.white_200, - marginBottom: 16, + marginBottom: props.theme.space.xs, })); const RowContainer = styled.div({ @@ -35,10 +35,7 @@ const RowContainer = styled.div({ flexDirection: 'row', width: '100%', alignItems: 'flex-start', -}); - -const AddressContainer = styled.div({ - marginTop: 12, + marginBottom: 12, }); const Icon = styled.img((props) => ({ @@ -209,29 +206,31 @@ function RecipientComponent({ )} {heading && {heading}} - - {renderIcon()} - {title} - {currencyType === 'NFT' || currencyType === 'Ordinal' || currencyType === 'RareSat' ? ( - - {value} - {valueDetail && {valueDetail}} - - ) : ( - - {amount}} - /> - {getFiatAmountString(new BigNumber(fiatAmount!))} - - )} - + {value && ( + + {renderIcon()} + {title} + {currencyType === 'NFT' || currencyType === 'Ordinal' || currencyType === 'RareSat' ? ( + + {value} + {valueDetail && {valueDetail}} + + ) : ( + + {amount}} + /> + {getFiatAmountString(new BigNumber(fiatAmount!))} + + )} + + )} {address && ( - +
{showSenderAddress ? ( @@ -241,7 +240,7 @@ function RecipientComponent({ ) : ( )} - +
)}
); diff --git a/src/app/hooks/queries/ordinals/useAddressRareSats.ts b/src/app/hooks/queries/ordinals/useAddressRareSats.ts index 0329325dc..3e21ff647 100644 --- a/src/app/hooks/queries/ordinals/useAddressRareSats.ts +++ b/src/app/hooks/queries/ordinals/useAddressRareSats.ts @@ -1,8 +1,11 @@ import useWalletSelector from '@hooks/useWalletSelector'; -import { getAddressUtxoOrdinalBundles, getUtxoOrdinalBundle } from '@secretkeylabs/xverse-core'; +import { + getAddressUtxoOrdinalBundles, + getUtxoOrdinalBundle, + mapRareSatsAPIResponseToBundle, +} from '@secretkeylabs/xverse-core'; import { useInfiniteQuery, useQuery } from '@tanstack/react-query'; import { handleRetries, InvalidParamsError } from '@utils/query'; -import { mapRareSatsAPIResponseToRareSats } from '@utils/rareSats'; const PAGE_SIZE = 30; @@ -39,7 +42,11 @@ export const useAddressRareSats = () => { }); }; -export const useGetUtxoOrdinalBundle = (output?: string, shouldMakeTheCall?: boolean) => { +export const useGetUtxoOrdinalBundle = ( + output?: string, + shouldMakeTheCall?: boolean, + ordinalNumber?: number, +) => { const { network } = useWalletSelector(); const getUtxoOrdinalBundleByOutput = async () => { if (!output) { @@ -58,11 +65,21 @@ export const useGetUtxoOrdinalBundle = (output?: string, shouldMakeTheCall?: boo retry: handleRetries, staleTime: 1 * 60 * 1000, // 1 min }); - const bundle = data?.txid ? mapRareSatsAPIResponseToRareSats(data) : undefined; + + const bundle = data?.txid ? mapRareSatsAPIResponseToBundle(data) : undefined; + const inscriptionRange = bundle?.satRanges.find((range) => + range.inscriptions.some((inscription) => inscription.inscription_number === ordinalNumber), + ); + const ordinalSatributes = + inscriptionRange?.satributes.filter((satribute) => satribute !== 'COMMON') ?? []; + const exoticRangesCount = (bundle?.satributes.filter((range) => !range.includes('COMMON')) ?? []) + .length; + const isPartOfABundle = exoticRangesCount > ordinalSatributes.length; return { bundle, - isPartOfABundle: (bundle?.items ?? []).length > 1, + isPartOfABundle, + ordinalSatributes, isLoading, }; }; diff --git a/src/app/hooks/useDetectOrdinalInSignPsbt.ts b/src/app/hooks/useDetectOrdinalInSignPsbt.ts index 66fe5cdeb..b570729cf 100644 --- a/src/app/hooks/useDetectOrdinalInSignPsbt.ts +++ b/src/app/hooks/useDetectOrdinalInSignPsbt.ts @@ -1,35 +1,33 @@ -import { getUtxoOrdinalBundle, ParsedPSBT } from '@secretkeylabs/xverse-core'; -import { BundleItem, mapRareSatsAPIResponseToRareSats } from '@utils/rareSats'; -import { isAxiosError } from 'axios'; +import { + Bundle, + BundleSatRange, + getUtxoOrdinalBundle, + mapRareSatsAPIResponseToBundle, + ParsedPSBT, +} from '@secretkeylabs/xverse-core'; import useWalletSelector from './useWalletSelector'; +export type InputsBundle = Pick; + const useDetectOrdinalInSignPsbt = () => { const { ordinalsAddress, network } = useWalletSelector(); const handleOrdinalAndOrdinalInfo = async (parsedPsbt?: ParsedPSBT) => { - const bundleItems: BundleItem[] = []; + const satRanges: BundleSatRange[] = []; + let value = 0; + let totalExoticSats = 0; let userReceivesOrdinal = false; if (parsedPsbt) { - parsedPsbt.inputs.map(async (input) => { - try { - const data = await getUtxoOrdinalBundle(network.type, input.txid, input.index); - - const bundle = mapRareSatsAPIResponseToRareSats(data); - bundle.items.forEach((item) => { - // we don't show unknown items for now - if (item.type === 'unknown') { - return; - } - bundleItems.push(item); - }); - } catch (e) { - // we get back a 404 if the UTXO is not found, so it is likely this is a UTXO from an unpublished txn - if (!isAxiosError(e) || e.response?.status !== 404) { - // rethrow error if response was not 404 - throw e; - } - } + const inputsRequest = parsedPsbt.inputs.map((input) => + getUtxoOrdinalBundle(network.type, input.txid, input.index), + ); + const inputsResponse = await Promise.all(inputsRequest); + inputsResponse.forEach((inputResponse) => { + const bundle = mapRareSatsAPIResponseToBundle(inputResponse); + value += bundle.value; + totalExoticSats += bundle.totalExoticSats; + satRanges.push(...bundle.satRanges); }); parsedPsbt.outputs.forEach((output) => { @@ -39,8 +37,14 @@ const useDetectOrdinalInSignPsbt = () => { }); } + const bundleItemsData = { + value, + satRanges, + totalExoticSats, + }; + return { - bundleItemsData: bundleItems, + bundleItemsData, userReceivesOrdinal, }; }; diff --git a/src/app/layouts/sendLayout.tsx b/src/app/layouts/sendLayout.tsx index 8a077ce8b..dc5ca3537 100644 --- a/src/app/layouts/sendLayout.tsx +++ b/src/app/layouts/sendLayout.tsx @@ -8,6 +8,10 @@ import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; import { breakpoints, devices } from 'theme'; +interface ContainerProps { + isGallery?: boolean; +} + const ScrollContainer = styled.div((props) => ({ display: 'flex', flex: 1, @@ -15,7 +19,7 @@ const ScrollContainer = styled.div((props) => ({ ...props.theme.scrollbar, })); -const Container = styled.div` +const Container = styled.div` display: flex; flex-direction: column; margin: auto; @@ -31,7 +35,9 @@ const Container = styled.div` max-width: 588px; border: 1px solid ${(props) => props.theme.colors.elevation3}; border-radius: ${(props) => props.theme.space.s}; - padding: ${(props) => props.theme.space.l} ${(props) => props.theme.space.m}; + padding-top: ${(props) => props.theme.space.l}; + padding-left: ${(props) => (props.isGallery ? props.theme.space.m : 0)}; + padding-right: ${(props) => (props.isGallery ? props.theme.space.m : 0)}; padding-bottom: ${(props) => props.theme.space.xxl}; margin-top: ${(props) => props.theme.space.xxxxl}; } @@ -51,6 +57,7 @@ const Button = styled.button` display: flex; background-color: transparent; margin-bottom: ${(props) => props.theme.space.l}; + margin-left: ${(props) => props.theme.space.s}; `; function SendLayout({ @@ -66,6 +73,7 @@ function SendLayout({ const { t } = useTranslation('translation', { keyPrefix: 'SEND' }); const isScreenLargerThanXs = document.documentElement.clientWidth > Number(breakpoints.xs); const year = new Date().getFullYear(); + const isGalleryOpen: boolean = document.documentElement.clientWidth > 360; return ( <> @@ -75,7 +83,7 @@ function SendLayout({ )} - + {isScreenLargerThanXs && !hideBackButton && onClickBack && ( - */} - - )} - - + + + {selectedOrdinal && ( - {selectedSatBundle && isRareSat ? ( - - ) : ( - - )} + - - - {!isGalleryOpen && ( - - - )} - - + + ); } export default ConfirmOrdinalTransaction; diff --git a/src/app/screens/login/index.tsx b/src/app/screens/login/index.tsx index 9370dded4..d97e46dd0 100644 --- a/src/app/screens/login/index.tsx +++ b/src/app/screens/login/index.tsx @@ -3,10 +3,10 @@ import EyeSlash from '@assets/img/createPassword/EyeSlash.svg'; import logo from '@assets/img/xverse_logo.svg'; import ActionButton from '@components/button'; import useWalletReducer from '@hooks/useWalletReducer'; -import { animated,useSpring } from '@react-spring/web'; +import { animated, useSpring } from '@react-spring/web'; import MigrationConfirmation from '@screens/migrationConfirmation'; import { addMinutes } from 'date-fns'; -import { useEffect,useState } from 'react'; +import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import styled from 'styled-components'; diff --git a/src/app/screens/nftDashboard/collectiblesTabs.tsx b/src/app/screens/nftDashboard/collectiblesTabs.tsx index e2182443c..c68db4df1 100644 --- a/src/app/screens/nftDashboard/collectiblesTabs.tsx +++ b/src/app/screens/nftDashboard/collectiblesTabs.tsx @@ -1,7 +1,11 @@ import ActionButton from '@components/button'; import WrenchErrorMessage from '@components/wrenchErrorMessage'; +import { + Bundle, + mapRareSatsAPIResponseToBundle, + UtxoOrdinalBundle, +} from '@secretkeylabs/xverse-core'; import { StyledP, StyledTab, StyledTabList } from '@ui-library/common.styled'; -import { ApiBundle, Bundle, mapRareSatsAPIResponseToRareSats } from '@utils/rareSats'; import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate, useSearchParams } from 'react-router-dom'; @@ -12,6 +16,9 @@ import Notice from './notice'; import RareSatsTabGridItem from './rareSatsTabGridItem'; import type { NftDashboardState } from './useNftDashboard'; +const MAX_SATS_ITEMS_EXTENSION = 5; +const MAX_SATS_ITEMS_GALLERY = 20; + export const GridContainer = styled.div<{ isGalleryOpen: boolean; }>((props) => ({ @@ -24,6 +31,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 StickyStyledTabList = styled(StyledTabList)` position: sticky; background: ${(props) => props.theme.colors.elevation0}; @@ -227,17 +240,21 @@ export default function CollectiblesTabs({ {rareSatsQuery.isInitialLoading ? ( ) : ( - + {!rareSatsQuery.error && !rareSatsQuery.isLoading && rareSatsQuery.data?.pages ?.map((page) => page?.results) .flat() - .map((utxo: ApiBundle) => mapRareSatsAPIResponseToRareSats(utxo)) + .map((utxo: UtxoOrdinalBundle) => mapRareSatsAPIResponseToBundle(utxo)) .map((bundle: Bundle) => ( - + ))} - + )} {rareSatsQuery.hasNextPage && ( diff --git a/src/app/screens/nftDashboard/inscriptionsTabGridItem.tsx b/src/app/screens/nftDashboard/inscriptionsTabGridItem.tsx index 518c43b26..3610ebb3d 100644 --- a/src/app/screens/nftDashboard/inscriptionsTabGridItem.tsx +++ b/src/app/screens/nftDashboard/inscriptionsTabGridItem.tsx @@ -1,5 +1,4 @@ import CollectibleCollage from '@components/collectibleCollage/collectibleCollage'; -import RareSatAsset from '@components/rareSatAsset/rareSatAsset'; import OrdinalImage from '@screens/ordinals/ordinalImage'; import { InscriptionCollectionsData } from '@secretkeylabs/xverse-core'; import { StyledP } from '@ui-library/common.styled'; @@ -8,7 +7,6 @@ import { getInscriptionsTabGridItemId, getInscriptionsTabGridItemSubText, isCollection, - mapCondensedInscriptionToBundleItem, } from '@utils/inscriptions'; import { useNavigate } from 'react-router-dom'; import styled from 'styled-components'; @@ -76,15 +74,11 @@ export function InscriptionsTabGridItem({ {!collection.thumbnail_inscriptions ? ( // eslint-disable-line no-nested-ternary ) : !isCollection(collection) || collection.thumbnail_inscriptions.length === 1 ? ( // eslint-disable-line no-nested-ternary - + ) : collection.category === 'brc-20' ? ( ) : ( - + )} diff --git a/src/app/screens/nftDashboard/rareSatsTabGridItem.tsx b/src/app/screens/nftDashboard/rareSatsTabGridItem.tsx index eb8228b36..68d2e0601 100644 --- a/src/app/screens/nftDashboard/rareSatsTabGridItem.tsx +++ b/src/app/screens/nftDashboard/rareSatsTabGridItem.tsx @@ -1,70 +1,127 @@ -import BundleAsset from '@components/bundleAsset/bundleAsset'; +import ExoticSatsRow from '@components/exoticSatsRow/exoticSatsRow'; +import RareSatIcon from '@components/rareSatIcon/rareSatIcon'; import useSatBundleDataReducer from '@hooks/stores/useSatBundleReducer'; +import { DotsThree } from '@phosphor-icons/react'; +import { Bundle, RareSatsType } from '@secretkeylabs/xverse-core'; import { StyledP } from '@ui-library/common.styled'; -import { Bundle, getBundleId, getBundleSubText } from '@utils/rareSats'; +import { getFormattedTxIdVoutFromBundle } from '@utils/rareSats'; import { useNavigate } from 'react-router-dom'; import styled from 'styled-components'; +import Theme from 'theme'; -const InfoContainer = styled.div` +const Range = styled.div` display: flex; - flex-direction: column; - align-items: flex-start; - width: 100%; + flex-direction: row; + border-radius: 6px; + border: 1px solid ${(props) => props.theme.colors.white_800}; + margin-left: 2px; + align-items: center; + padding: 1px; `; -const StyledBundleId = styled(StyledP)` - text-align: left; - text-wrap: nowrap; - overflow: hidden; - width: 100%; +const TileText = styled(StyledP)` + text-align: center; + color: ${(props) => props.theme.colors.white_200}; + padding: 2px 3px; `; -const StyledBundleSub = styled(StyledP)` - text-align: left; - text-overflow: ellipsis; - text-wrap: nowrap; - overflow: hidden; - width: 100%; -`; - -const GridItemContainer = styled.button` - display: flex; - flex-direction: column; - background: transparent; - gap: ${(props) => props.theme.space.s}; - width: 100%; -`; +const Pressable = styled.button((props) => ({ + background: 'transparent', + width: '100%', + marginBottom: props.theme.space.s, +})); -function RareSatsTabGridItem({ bundle }: { bundle: Bundle }) { +function RareSatsTabGridItem({ bundle, maxItems }: { bundle: Bundle; maxItems: number }) { const navigate = useNavigate(); - const { setSelectedSatBundleDetails, setSelectedSatBundleItemIndex } = useSatBundleDataReducer(); - const isMoreThanOneItem = bundle.items?.length > 1; + const { setSelectedSatBundleDetails } = useSatBundleDataReducer(); const handleOnClick = () => { + // exotics v1 wont show range details only bundle details setSelectedSatBundleDetails(bundle); - if (isMoreThanOneItem) { - return navigate('/nft-dashboard/rare-sats-bundle'); + navigate('/nft-dashboard/rare-sats-bundle', { state: { source: 'RareSatsTab' } }); + }; + + const renderedIcons = () => { + let totalIconsDisplayed = 0; + let totalTilesDisplayed = 0; + + const icons: (RareSatsType | 'ellipsis' | '+X')[][] = []; + let overLimitSatsIndex: number | null = null; + let totalSats = 0; + let totalTiles = 0; + bundle.satributes + .filter((satributes) => !(satributes.includes('COMMON') && bundle.satributes.length > 1)) + .forEach((sats, index) => { + totalSats += sats.length; + totalTiles += 1; + + const isOverLimit = + totalIconsDisplayed + sats.length > maxItems - (sats.length > 1 ? 2 : 1); + // we add ranges till we reach the limit and we store the index of the range that is over the limit + if (isOverLimit || overLimitSatsIndex !== null) { + overLimitSatsIndex = overLimitSatsIndex !== null ? overLimitSatsIndex : index; + return; + } + totalTilesDisplayed += 1; + totalIconsDisplayed += sats.length; + icons.push(sats); + }); + + // if we have more than 1 range and we have reached the limit we add ellipsis and +X + if (overLimitSatsIndex !== null) { + const sats = bundle.satributes[overLimitSatsIndex]; + const satsToDisplay = maxItems - totalIconsDisplayed - 2; + const firstSats = sats.slice(0, satsToDisplay); + // we add ellipsis only if we have more than 1 slot left counting the +X + if (firstSats.length > 0) { + totalTilesDisplayed += 1; + icons.push([...firstSats, 'ellipsis']); + } + + if (totalSats > maxItems) { + icons.push(['+X']); + } } - setSelectedSatBundleItemIndex(0); - navigate('/nft-dashboard/rare-sats-detail'); + return icons.map((sats, index) => ( + + {sats.map((sattribute, indexSatributes) => { + if (sattribute === 'ellipsis') { + return ( + + ); + } + + if (sattribute === '+X') { + return ( + + +{totalTiles - totalTilesDisplayed} + + ); + } + // eslint-disable-next-line react/no-array-index-key + return ; + })} + + )); }; - const bundleId = getBundleId(bundle); - const bundleSubText = getBundleSubText(bundle); + const bundleId = getFormattedTxIdVoutFromBundle(bundle); return ( - - - - - {bundleId} - - - {bundleSubText} - - - + + + ); } export default RareSatsTabGridItem; diff --git a/src/app/screens/nftDashboard/supportedRarities/index.tsx b/src/app/screens/nftDashboard/supportedRarities/index.tsx index 43cd55e69..ce91f4cda 100644 --- a/src/app/screens/nftDashboard/supportedRarities/index.tsx +++ b/src/app/screens/nftDashboard/supportedRarities/index.tsx @@ -1,12 +1,11 @@ -import { useTranslation } from 'react-i18next'; -import { ArrowUpRight } from '@phosphor-icons/react'; -import styled from 'styled-components'; import TopRow from '@components/topRow'; -import { useNavigate } from 'react-router-dom'; +import { ArrowUpRight } from '@phosphor-icons/react'; +import { RodarmorRareSats, Satributes } from '@secretkeylabs/xverse-core'; import { StyledP } from '@ui-library/common.styled'; -import { RareSats } from '@utils/rareSats'; -import { useMemo } from 'react'; import { BLOG_LINK } from '@utils/constants'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; +import styled from 'styled-components'; import Theme from 'theme'; import RarityTile from './rarityTile'; @@ -46,21 +45,22 @@ const Container = styled.div((props) => ({ width: props.isGallery ? 580 : '100%', })); -const MainContainer = styled.div({ +const MainContainer = styled.div((props) => ({ + ...props.theme.scrollbar, display: 'flex', flexDirection: 'column', alignItems: 'center', width: '100%', - height: '100%', -}); + backgroundColor: Theme.colors.elevation0, +})); -const rarityTypes = RareSats.filter((rareSat) => rareSat !== 'common'); +const rarityTypes = [...RodarmorRareSats, ...Satributes]; function SupportedRarities() { const navigate = useNavigate(); - const { t } = useTranslation('translation', { keyPrefix: 'NFT_DASHBOARD_SCREEN' }); + const { t } = useTranslation('translation', { keyPrefix: 'RARE_SATS' }); - const isGalleryOpen: boolean = useMemo(() => document.documentElement.clientWidth > 360, []); + const isGalleryOpen: boolean = document.documentElement.clientWidth > 360; const openLearnMoreLink = () => window.open(`${BLOG_LINK}/rare-satoshis`, '_blank', 'noopener,noreferrer'); @@ -80,7 +80,7 @@ function SupportedRarities() { {t('RARITY_DETAIL.LEARN_MORE')} - + diff --git a/src/app/screens/nftDashboard/supportedRarities/rarityTile.tsx b/src/app/screens/nftDashboard/supportedRarities/rarityTile.tsx index dd5acb1a2..b54809b8a 100644 --- a/src/app/screens/nftDashboard/supportedRarities/rarityTile.tsx +++ b/src/app/screens/nftDashboard/supportedRarities/rarityTile.tsx @@ -1,11 +1,8 @@ +import RareSatIcon from '@components/rareSatIcon/rareSatIcon'; +import { RareSatsType } from '@secretkeylabs/xverse-core'; +import { getRareSatsLabelByType } from '@utils/rareSats'; import { useTranslation } from 'react-i18next'; -import { ArrowUpRight } from '@phosphor-icons/react'; import styled from 'styled-components'; -import { RareSatsType } from '@utils/rareSats'; -import RareSatIcon from '@components/rareSatIcon/rareSatIcon'; -import useWalletSelector from '@hooks/useWalletSelector'; -import { MAGISAT_IO_RARITY_SCAN_URL } from '@utils/constants'; -import Theme from 'theme'; const Container = styled.div((props) => ({ display: 'flex', @@ -25,27 +22,13 @@ const TextsColumn = styled.div((props) => ({ const RarityText = styled.p((props) => ({ ...props.theme.typography.body_bold_m, - color: props.theme.colors.white[0], + color: props.theme.colors.white_0, textTransform: 'capitalize', })); const RarityDetailText = styled.p((props) => ({ ...props.theme.typography.body_medium_m, - color: props.theme.colors.white[200], -})); - -const ButtonText = styled.p((props) => ({ - ...props.theme.typography.body_medium_m, - color: props.theme.colors.orange_main, - marginRight: props.theme.spacing(2), -})); - -const ButtonImage = styled.button((props) => ({ - backgroundColor: 'transparent', - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - marginTop: props.theme.spacing(2), + color: props.theme.colors.white_200, })); interface Props { @@ -53,24 +36,14 @@ interface Props { } function RarityTile({ type }: Props) { - const { ordinalsAddress } = useWalletSelector(); - const { t } = useTranslation('translation', { keyPrefix: 'NFT_DASHBOARD_SCREEN' }); - - const openScanLink = () => - window.open(`${MAGISAT_IO_RARITY_SCAN_URL}${ordinalsAddress}`, '_blank', 'noopener,noreferrer'); + const { t } = useTranslation('translation', { keyPrefix: 'RARE_SATS' }); return ( - + - {type} + {getRareSatsLabelByType(type)} {t(`RARITY_DETAIL.${type.toUpperCase()}`)} - {type === 'unknown' && ( - - {t('RARITY_DETAIL.SCAN')} - - - )} ); diff --git a/src/app/screens/ordinalDetail/index.tsx b/src/app/screens/ordinalDetail/index.tsx index 53a5279f8..13d056b27 100644 --- a/src/app/screens/ordinalDetail/index.tsx +++ b/src/app/screens/ordinalDetail/index.tsx @@ -5,14 +5,18 @@ import AlertMessage from '@components/alertMessage'; import { BetterBarLoader } from '@components/barLoader'; import ActionButton from '@components/button'; import CollectibleDetailTile from '@components/collectibleDetailTile'; +import RareSatIcon from '@components/rareSatIcon/rareSatIcon'; import Separator from '@components/separator'; import SquareButton from '@components/squareButton'; import BottomTabBar from '@components/tabBar'; import TopRow from '@components/topRow'; import WebGalleryButton from '@components/webGalleryButton'; import { useResetUserFlow } from '@hooks/useResetUserFlow'; -import { ArrowRight, ArrowUp, CubeTransparent, Share } from '@phosphor-icons/react'; +import { ArrowUp, Share } from '@phosphor-icons/react'; import OrdinalImage from '@screens/ordinals/ordinalImage'; +import Callout from '@ui-library/callout'; +import { StyledP } from '@ui-library/common.styled'; +import { getRareSatsColorsByRareSatsType, getRareSatsLabelByType } from '@utils/rareSats'; import { useTranslation } from 'react-i18next'; import { Tooltip } from 'react-tooltip'; import styled from 'styled-components'; @@ -89,8 +93,8 @@ const ExtensionOrdinalsContainer = styled.div((props) => ({ justifyContent: 'center', alignItems: 'center', borderRadius: props.theme.radius(1), - marginBottom: props.theme.spacing(12), marginTop: props.theme.spacing(12), + marginBottom: props.theme.space.m, })); const OrdinalTitleText = styled.h1((props) => ({ @@ -109,7 +113,6 @@ const DescriptionText = styled.h1((props) => ({ ...props.theme.typography.headline_l, color: props.theme.colors.white_0, fontSize: 24, - marginBottom: props.theme.spacing(8), })); const NftOwnedByText = styled.h1((props) => ({ @@ -164,7 +167,7 @@ const MintLimitContainer = styled.div((props) => ({ marginLeft: props.theme.spacing(30), })); -const DescriptionContainer = styled.h1((props) => ({ +const DescriptionContainer = styled.div((props) => ({ display: 'flex', flexDirection: 'column', marginBottom: props.theme.spacing(30), @@ -270,40 +273,6 @@ const Text = styled.h1((props) => ({ marginLeft: props.theme.spacing(2), })); -const RareSatsBundleContainer = styled.div((props) => ({ - display: 'flex', - flex: 1, - flexDirection: 'row', - padding: props.theme.spacing(8), - marginBottom: props.theme.spacing(8), - border: `1px solid ${props.theme.colors.white_800}`, - borderRadius: '12px', -})); -const CubeTransparentIcon = styled(CubeTransparent)((props) => ({ - color: props.theme.colors.white_200, - marginRight: props.theme.spacing(8), -})); -const RareSatsBundleTextDescription = styled.div((props) => ({ - ...props.theme.typography.body_m, - color: props.theme.colors.white_200, -})); -const BundleLinkContainer = styled.button((props) => ({ - display: 'inline-flex', - flexDirection: 'row', - alignItems: 'center', - marginTop: props.theme.spacing(4), - backgroundColor: 'transparent', - color: props.theme.colors.white_0, - transition: 'background-color 0.2s ease, opacity 0.2s ease', - ':hover': { - color: props.theme.colors.white_200, - }, -})); -const BundleLinkText = styled.div((props) => ({ - ...props.theme.typography.body_medium_m, - marginRight: props.theme.spacing(1), -})); - const GalleryButtonContainer = styled.div` width: 190px; border-radius: 12px; @@ -314,9 +283,12 @@ const RowButtonContainer = styled.div((props) => ({ flexDirection: 'row', justifyContent: 'center', columnGap: props.theme.spacing(11), - paddingBottom: props.theme.spacing(16), - marginBottom: props.theme.spacing(4), - marginTop: props.theme.spacing(4), + marginBottom: props.theme.space.l, + marginTop: props.theme.space.m, + width: '100%', +})); + +const Divider = styled.div((props) => ({ width: '100%', borderBottom: `1px solid ${props.theme.colors.elevation3}`, })); @@ -379,8 +351,49 @@ const InfoContainer = styled.div((props) => ({ padding: `0 ${props.theme.spacing(8)}px`, })); +const RareSatsBundleCallout = styled(Callout)((props) => ({ + width: props.isGallery ? 400 : '100%', + marginBottom: props.isGallery ? 0 : props.theme.space.l, + marginTop: props.isGallery ? props.theme.space.xs : 0, +})); + +const SatributesIconsContainer = styled.div((props) => ({ + display: 'inline-flex', + flexDirection: 'row', + marginTop: props.isGallery ? props.theme.space.m : 0, +})); + +const SatributesBadgeContainer = styled.div((props) => ({ + marginTop: props.isGallery ? 0 : props.theme.space.m, +})); +const SatributesBadges = styled.div((props) => ({ + display: 'inline-flex', + flexDirection: 'row', + flexWrap: 'wrap', + maxWidth: props.isGallery ? 400 : '100%', + marginTop: props.theme.space.s, +})); +const Badge = styled.div<{ backgroundColor?: string; isLastItem: boolean }>((props) => ({ + display: 'inline-flex', + flexDirection: 'row', + alignItems: 'center', + backgroundColor: props.backgroundColor, + padding: `${props.theme.space.s} ${props.theme.space.s}`, + borderRadius: props.theme.radius(2), + border: `1px solid ${props.theme.colors.elevation3}`, + marginRight: props.isLastItem ? 0 : props.theme.space.xs, + marginBottom: props.theme.space.xs, +})); +const SatributeBadgeLabel = styled(StyledP)` + margin-left: ${(props) => props.theme.space.xs}; +`; +const DataItemsContainer = styled.div` + margin-top: ${(props) => props.theme.space.l}; +`; + function OrdinalDetailScreen() { const { t } = useTranslation('translation', { keyPrefix: 'NFT_DETAIL_SCREEN' }); + const { t: commonT } = useTranslation('translation', { keyPrefix: 'COMMON' }); const ordinalDetails = useOrdinalDetail(); const { ordinal, @@ -390,6 +403,7 @@ function OrdinalDetailScreen() { showSendOridnalsAlert, isBrc20Ordinal, isPartOfABundle, + ordinalSatributes, isGalleryOpen, brc20InscriptionStatus, brc20InscriptionStatusColor, @@ -573,18 +587,45 @@ function OrdinalDetailScreen() { }; const rareSats = isPartOfABundle && ( - - -
- - {t('RARE_SATS_BUNDLE_DESCRIPTION')} - - - {t('RARE_SATS_BUNDLE_LINK')} - - -
-
+ + ); + + const showSatributes = ordinalSatributes.length > 0; + const satributesIcons = showSatributes && ( + + {ordinalSatributes.map((satribute) => ( + + ))} + + ); + const stributesBadges = showSatributes && ( + + + {commonT('SATTRIBUTES')} + + + {ordinalSatributes.map((satribute, index) => { + const backgroundColor = getRareSatsColorsByRareSatsType(satribute) ?? 'transparent'; + return ( + = ordinalSatributes.length} + > + + + {getRareSatsLabelByType(satribute)} + + + ); + })} + + ); const extensionView = isLoading ? ( @@ -627,6 +668,7 @@ function OrdinalDetailScreen() { + {satributesIcons} } @@ -649,6 +691,8 @@ function OrdinalDetailScreen() { /> {rareSats} + + {stributesBadges} {isBrc20Ordinal ? showBrc20OrdinalDetail(false) : ordinalDetailAttributes} {t('VIEW_IN')} @@ -714,6 +758,7 @@ function OrdinalDetailScreen() { : ordinal?.collection_name || t('INSCRIPTION')} {ordinal?.number} + {satributesIcons} {t('OWNED_BY')} {`${ordinalsAddress.substring( @@ -752,7 +797,10 @@ function OrdinalDetailScreen() { {t('DATA')} {rareSats} - {isBrc20Ordinal ? showBrc20OrdinalDetail(true) : ordinalDetailAttributes} + + {stributesBadges} + {isBrc20Ordinal ? showBrc20OrdinalDetail(true) : ordinalDetailAttributes} + {t('VIEW_IN')} {t('ORDINAL_VIEWER')} diff --git a/src/app/screens/ordinalDetail/useOrdinalDetail.ts b/src/app/screens/ordinalDetail/useOrdinalDetail.ts index 7704e5b78..9ba917938 100644 --- a/src/app/screens/ordinalDetail/useOrdinalDetail.ts +++ b/src/app/screens/ordinalDetail/useOrdinalDetail.ts @@ -33,9 +33,10 @@ export default function useOrdinalDetail() { const { isPending, pendingTxHash } = usePendingOrdinalTxs(ordinalData?.tx_id); const textContent = useTextOrdinalContent(ordinalData!); const { setSelectedSatBundleDetails } = useSatBundleDataReducer(); - const { bundle, isPartOfABundle } = useGetUtxoOrdinalBundle( + const { bundle, isPartOfABundle, ordinalSatributes } = useGetUtxoOrdinalBundle( ordinalData?.output, hasActivatedRareSatsKey, + ordinalData?.number, ); const theme = useTheme(); const { t } = useTranslation('translation', { keyPrefix: 'NFT_DETAIL_SCREEN' }); @@ -104,11 +105,12 @@ export default function useOrdinalDetail() { }; const handleNavigationToRareSatsBundle = () => { - if (!bundle) { + if (!bundle || !ordinalData) { return; } + setSelectedOrdinalDetails(ordinalData); setSelectedSatBundleDetails(bundle); - navigate('/nft-dashboard/rare-sats-bundle'); + navigate('/nft-dashboard/rare-sats-bundle', { state: { source: 'OrdinalDetail' } }); }; const onCopyClick = () => { @@ -128,7 +130,8 @@ export default function useOrdinalDetail() { ordinalsAddress, showSendOridnalsAlert, isBrc20Ordinal, - isPartOfABundle, + isPartOfABundle: isPartOfABundle && hasActivatedRareSatsKey, + ordinalSatributes: hasActivatedRareSatsKey ? ordinalSatributes : [], isGalleryOpen, brc20InscriptionStatus, brc20InscriptionStatusColor, diff --git a/src/app/screens/rareSatsBundle/index.tsx b/src/app/screens/rareSatsBundle/index.tsx index 985bf887b..c6beccec5 100644 --- a/src/app/screens/rareSatsBundle/index.tsx +++ b/src/app/screens/rareSatsBundle/index.tsx @@ -12,13 +12,17 @@ import useSatBundleDataReducer from '@hooks/stores/useSatBundleReducer'; import { useResetUserFlow } from '@hooks/useResetUserFlow'; import useWalletSelector from '@hooks/useWalletSelector'; import { ArrowRight, ArrowUp } from '@phosphor-icons/react'; -import { GridContainer } from '@screens/nftDashboard/collectiblesTabs'; +import { BundleSatRange } from '@secretkeylabs/xverse-core'; import { StyledHeading, StyledP } from '@ui-library/common.styled'; -import { getBtcTxStatusUrl, isInOptions, isLedgerAccount } from '@utils/helper'; -import { BundleItem } from '@utils/rareSats'; +import { + getBtcTxStatusUrl, + getTruncatedAddress, + isInOptions, + isLedgerAccount, +} from '@utils/helper'; import { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useNavigate } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import styled from 'styled-components'; import OrdinalAttributeComponent from '../ordinalDetail/ordinalAttributeComponent'; import { RareSatsBundleGridItem } from './rareSatsBundleGridItem'; @@ -29,11 +33,13 @@ interface DetailSectionProps { /* layout */ const Container = styled.div` + ...${(props) => props.theme.scrollbar}; overflow-y: auto; + padding-bottom: ${(props) => props.theme.space.l}; `; const PageHeader = styled.div` - padding: ${(props) => props.theme.space.m}; + padding: ${(props) => (props.isGalleryOpen ? props.theme.space.m : 0)}; padding-top: 0; max-width: 1224px; margin-top: ${(props) => (props.isGalleryOpen ? props.theme.space.xxl : props.theme.space.l)}; @@ -47,27 +53,17 @@ const PageHeaderContent = styled.div` display: flex; flex-direction: ${(props) => (props.isGalleryOpen ? 'row' : 'column')}; justify-content: ${(props) => (props.isGalleryOpen ? 'space-between' : 'initial')}; - row-gap: ${(props) => props.theme.space.xl}; `; -const AttributesContainer = styled.div` - max-width: 285px; -`; +const AttributesContainer = styled.div((props) => ({ + maxWidth: props.isGalleryOpen ? '285px' : '100%', + padding: props.isGalleryOpen ? 0 : `0 ${props.theme.space.m}`, +})); const StyledSeparator = styled(Separator)` margin-bottom: ${(props) => props.theme.space.xxl}; `; -const StyledGridContainer = styled(GridContainer)` - margin-top: ${(props) => props.theme.spacing(8)}px; - padding: 0 ${(props) => props.theme.space.m}; - padding-bottom: ${(props) => props.theme.space.xl}; - max-width: 1224px; - margin-left: auto; - margin-right: auto; - width: 100%; -`; - /* components */ const StyledWebGalleryButton = styled(WebGalleryButton)` @@ -138,11 +134,39 @@ const NoCollectiblesText = styled.p((props) => ({ textAlign: 'center', })); +const Header = styled.div<{ isGalleryOpen: boolean }>((props) => ({ + display: props.isGalleryOpen ? 'block' : 'flex', + flexDirection: props.isGalleryOpen ? 'row' : 'column', + alignItems: props.isGalleryOpen ? 'flex-start' : 'center', +})); + +const SatRangeContainer = styled.div((props) => ({ + marginTop: props.isGalleryOpen ? 0 : props.theme.space.xl, + maxWidth: '1224px', + marginLeft: 'auto', + marginRight: 'auto', + width: '100%', +})); + +const DetailSection = styled.div((props) => ({ + display: 'flex', + flexDirection: props.isGalleryOpen ? 'column' : 'row', + justifyContent: 'space-between', + columnGap: props.theme.space.m, + width: '100%', +})); + +const SeeRarityContainer = styled.div` + padding: ${(props) => props.theme.space.l} ${(props) => props.theme.space.m}; +`; + function RareSatsBundle() { const { t } = useTranslation('translation'); const navigate = useNavigate(); - const { network, selectedAccount } = useWalletSelector(); - const { selectedSatBundle: bundle } = useNftDataSelector(); + const location = useLocation(); + const { source } = location.state || {}; + const { network, selectedAccount, ordinalsAddress } = useWalletSelector(); + const { selectedSatBundle: bundle, selectedOrdinal } = useNftDataSelector(); const { isPending, pendingTxHash } = usePendingOrdinalTxs(bundle?.txid); const [showSendOrdinalsAlert, setShowSendOrdinalsAlert] = useState(false); const { setSelectedSatBundleDetails } = useSatBundleDataReducer(); @@ -152,7 +176,11 @@ function RareSatsBundle() { useResetUserFlow('/rare-sats-bundle'); const handleBackButtonClick = () => { - navigate('/nft-dashboard?tab=rareSats'); + if (source === 'OrdinalDetail') { + navigate(-1); + } else { + navigate('/nft-dashboard?tab=rareSats'); + } setSelectedSatBundleDetails(null); }; @@ -191,7 +219,11 @@ function RareSatsBundle() { navigate('/nft-dashboard/supported-rarity-scale'); }; - const isEmpty = !bundle?.items?.length; + const isEmpty = !bundle?.satRanges?.length; + + const goBackText = selectedOrdinal?.id + ? t('SEND.MOVE_TO_ASSET_DETAIL') + : t('NFT_DETAIL_SCREEN.MOVE_TO_ASSET_DETAIL'); return ( <> @@ -207,20 +239,18 @@ function RareSatsBundle() { )} -
+
- {t('RARE_SATS.RARE_SATS_BUNDLE')} + {t('NFT_DASHBOARD_SCREEN.RARE_SATS')} - {t('NFT_DASHBOARD_SCREEN.TOTAL_ITEMS', { total: bundle?.items?.length })} + {bundle?.totalExoticSats} {!isGalleryOpen && } @@ -230,19 +260,47 @@ function RareSatsBundle() { onPress={handleSendOrdinal} /> - - - {t('RARE_SATS.RARITY_LINK_TEXT')} - - - -
- - + {isGalleryOpen && ( + + + {t('RARE_SATS.RARITY_LINK_TEXT')} + + + + )} + + {isEmpty && ( + {t('NFT_DASHBOARD_SCREEN.NO_COLLECTIBLES')} + )} + {!isGalleryOpen && ( + + {bundle?.satRanges.map((item: BundleSatRange) => ( + + ))} + + )} + {!isGalleryOpen && ( + + + + )} + + + + + {t('NFT_DASHBOARD_SCREEN.NO_COLLECTIBLES')} )} - - {bundle?.items?.map((item: BundleItem, index) => ( - - ))} - + {isGalleryOpen && ( + + {bundle?.satRanges.map((item: BundleSatRange) => ( + + ))} + + )} {showSendOrdinalsAlert && ( props.theme.radius(3)}px; - background: ${(props) => props.theme.colors.elevation1}; + border-radius: 6px; + border: 1px solid var(--white-800, rgba(255, 255, 255, 0.2)); + padding: 1px; + flex-wrap: wrap; `; -const GridItemContainer = styled.button` - display: flex; - flex-direction: column; - background: transparent; - gap: ${(props) => props.theme.space.s}; -`; - -export function RareSatsBundleGridItem({ - item, - itemIndex, -}: { - item: BundleItem; - itemIndex: number; -}) { - const navigate = useNavigate(); - const { setSelectedSatBundleItemIndex } = useSatBundleDataReducer(); - const { selectedSatBundle } = useNftDataSelector(); - - const handleOnClick = () => { - setSelectedSatBundleItemIndex(itemIndex); - navigate('/nft-dashboard/rare-sats-detail'); - }; - - const itemId = getBundleItemId(selectedSatBundle!, itemIndex); - const itemSubText = getBundleItemSubText({ - satType: item.type, - rareSatsType: item.rarity_ranking, - }); +const Container = styled.div((props) => ({ + marginBottom: props.theme.space.s, + padding: `0 ${props.theme.space.m}`, +})); +export function RareSatsBundleGridItem({ item }: { item: BundleSatRange }) { return ( - - - - - - - {itemId} - - - {itemSubText} - - - + + + + {item.satributes.map((satribute) => ( + + ))} + + + } + /> + ); } export default RareSatsBundleGridItem; diff --git a/src/app/screens/rareSatsDetail/rareSatsDetail.tsx b/src/app/screens/rareSatsDetail/rareSatsDetail.tsx index b5bb2b9a0..c3ee17fec 100644 --- a/src/app/screens/rareSatsDetail/rareSatsDetail.tsx +++ b/src/app/screens/rareSatsDetail/rareSatsDetail.tsx @@ -1,42 +1,12 @@ -import ArrowLeft from '@assets/img/dashboard/arrow_left.svg'; import AccountHeaderComponent from '@components/accountHeader'; -import AlertMessage from '@components/alertMessage'; -import ActionButton from '@components/button'; -import RareSatAsset from '@components/rareSatAsset/rareSatAsset'; import BottomTabBar from '@components/tabBar'; import TopRow from '@components/topRow'; -import WebGalleryButton from '@components/webGalleryButton'; -import usePendingOrdinalTxs from '@hooks/queries/usePendingOrdinalTx'; -import useNftDataSelector from '@hooks/stores/useNftDataSelector'; import useSatBundleDataReducer from '@hooks/stores/useSatBundleReducer'; import { useResetUserFlow } from '@hooks/useResetUserFlow'; -import useWalletSelector from '@hooks/useWalletSelector'; -import { ArrowRight, ArrowUp, Circle } from '@phosphor-icons/react'; -import Callout from '@ui-library/callout'; -import { XVERSE_ORDIVIEW_URL } from '@utils/constants'; -import { - getBtcTxStatusUrl, - getTruncatedAddress, - isInOptions, - isLedgerAccount, -} from '@utils/helper'; -import { - BundleItem, - getBundleItemId, - getBundleItemSubText, - getRareSatsColorsByRareSatsType, - getRareSatsLabelByType, - getRarityLabelByRareSatsType, -} from '@utils/rareSats'; -import { useMemo, useState } from 'react'; -import { useTranslation } from 'react-i18next'; +import { StyledP } from '@ui-library/common.styled'; +import { useMemo } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import styled from 'styled-components'; -import OrdinalAttributeComponent from '../ordinalDetail/ordinalAttributeComponent'; - -interface DetailSectionProps { - isGalleryOpen?: boolean; -} const Container = styled.div` display: flex; @@ -50,223 +20,19 @@ const Container = styled.div` } `; -const SendButtonContainer = styled.div` - width: ${(props) => (props.isGalleryOpen ? '222px' : '155px')}; -`; - -const BackButtonContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: 'row', - marginTop: props.theme.spacing(40), -})); - -const ExtensionContainer = styled.div({ - display: 'flex', - flexDirection: 'column', - marginTop: 8, - marginBottom: 40, - alignItems: 'center', - flex: 1, -}); - -const RareSatsContainer = styled.div((props) => ({ - maxWidth: 450, - width: '60%', - display: 'flex', - aspectRatio: '1', - flexDirection: 'column', - justifyContent: 'flex-start', - alignItems: 'flex-start', - borderRadius: 8, - marginBottom: props.theme.spacing(12), -})); - -const ExtensionRareSatsContainer = styled.div<{ isInscription?: boolean }>((props) => ({ - maxHeight: props.isInscription ? 148 : 64, - width: props.isInscription ? 148 : 64, - display: 'flex', - aspectRatio: '1', - justifyContent: 'center', - alignItems: 'center', - borderRadius: 8, - marginBottom: props.theme.spacing(12), - marginTop: props.theme.spacing(12), -})); - -const RareSatsTitleText = styled.h1((props) => ({ - ...props.theme.headline_m, - color: props.theme.colors.white['0'], - textAlign: 'center', -})); - -const RareSatsGalleryTitleText = styled.p((props) => ({ - ...props.theme.headline_l, - color: props.theme.colors.white['0'], - marginBottom: props.theme.spacing(12), -})); - -const DescriptionText = styled.p((props) => ({ - ...props.theme.headline_l, - color: props.theme.colors.white['0'], - fontSize: 24, - marginBottom: props.theme.spacing(16), -})); - const BottomBarContainer = styled.div({ marginTop: 'auto', }); -const RowContainer = styled.div((props) => ({ - display: 'flex', - alignItems: 'flex-start', - marginTop: props.theme.spacing(6), - flexDirection: 'row', -})); - -const ColumnContainer = styled.div({ - display: 'flex', - alignItems: 'flex-start', - flexDirection: 'column', - width: '100%', -}); - -const DescriptionContainer = styled.div((props) => ({ - display: 'flex', - flex: 1, - marginLeft: props.theme.spacing(20), - flexDirection: 'column', - marginBottom: props.theme.spacing(30), -})); - -const StyledWebGalleryButton = styled(WebGalleryButton)` - color: ${(props) => props.theme.colors.white_200}; - margin-top: ${(props) => props.theme.space.xs}; -`; - -const ButtonImage = styled.img((props) => ({ - marginRight: props.theme.spacing(3), - alignSelf: 'center', - transform: 'all', -})); - -const Button = styled.button((props) => ({ - display: 'flex', - flexDirection: 'row', - justifyContent: 'flex-start', - alignItems: 'center', - background: 'transparent', - marginBottom: props.theme.spacing(12), -})); - -const AssetDetailButtonText = styled.div((props) => ({ - ...props.theme.body_xs, - fontWeight: 400, - fontSize: 14, - color: props.theme.colors.white['0'], - textAlign: 'center', -})); - -const SatTypeText = styled.p((props) => ({ - ...props.theme[props.isGalleryOpen ? 'body_bold_l' : 'body_bold_m'], - color: props.theme.colors.white['400'], - textAlign: props.isGalleryOpen ? 'left' : 'center', - textTransform: 'capitalize', -})); - -const DetailSection = styled.div((props) => ({ - display: 'flex', - flexDirection: !props.isGalleryOpen ? 'row' : 'column', - justifyContent: 'space-between', - width: '100%', -})); - -const RareSatRankingBadge = styled.div<{ bgColor: string }>((props) => ({ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - borderRadius: '30px', - backgroundColor: props.bgColor, - marginTop: props.theme.spacing(1), - padding: '5px 10px', -})); - -const RareSatRankingBadgeText = styled.div((props) => ({ - ...props.theme.body_medium_s, - marginLeft: props.theme.spacing(4), -})); - -const StyledCallout = styled(Callout)((props) => ({ - marginBottom: props.theme.space.l, -})); - -const BundleRarityLinkContainer = styled.button((props) => ({ - marginTop: props.isGalleryOpen ? props.theme.space.l : props.theme.space.m, - display: 'inline-flex', - alignSelf: props.isGalleryOpen ? 'flex-start' : 'center', - flexDirection: 'row', - marginBottom: props.isGalleryOpen ? props.theme.spacing(14) : 0, - alignItems: 'center', - backgroundColor: 'transparent', - color: props.theme.colors.white_0, - transition: 'background-color 0.2s ease, opacity 0.2s ease', - ':hover': { - color: props.theme.colors.action.classicLight, - opacity: 0.6, - }, -})); -const BundleRarityTextLink = styled.p((props) => ({ - ...props.theme.body_medium_m, - color: props.theme.colors.white_200, - marginRight: props.theme.spacing(1), -})); -const ArrowRightIcon = styled(ArrowRight)((props) => ({ - color: props.theme.colors.white_200, -})); -const Divider = styled.div((props) => ({ - width: '100%', - height: '1px', - backgroundColor: props.theme.colors.white_900, - marginTop: props.theme.spacing(20), - marginBottom: props.theme.spacing(4), -})); -const Flex1 = styled.div(() => ({ - flex: 1, - width: '100%', -})); -const ViewInExplorerButton = styled.button((props) => ({ - display: 'flex', - flexDirection: 'row', - justifyContent: 'center', - alignItems: 'center', - backgroundColor: 'transparent', - marginTop: props.theme.space.xxl, - width: '100%', -})); - +// TODO: this screen will be re-implemented in future iteration of exotics sats function RareSatsDetailScreen() { - const { t } = useTranslation('translation'); const navigate = useNavigate(); const location = useLocation(); - const { ordinalsAddress, network, selectedAccount } = useWalletSelector(); - const { selectedSatBundle, selectedSatBundleItemIndex } = useNftDataSelector(); - const [showSendOridnalsAlert, setshowSendOridnalsAlert] = useState(false); + const { setSelectedSatBundleItemIndex } = useSatBundleDataReducer(); useResetUserFlow('/rare-sats-detail'); - const isGalleryOpen: boolean = useMemo(() => document.documentElement.clientWidth > 360, []); - - const bundle = selectedSatBundle!; - const { isPending, pendingTxHash } = usePendingOrdinalTxs(bundle.txid); - const itemIndex = selectedSatBundleItemIndex!; - const item = bundle.items[itemIndex] as BundleItem | undefined; - // when going back, selectedSatBundleItemIndex is set tu null and we don't want to render anything - if (!item) { - return null; - } - - const isBundle = bundle.items.length < 2; - const isUnknown = item?.type === 'unknown'; - const isInscription = item?.type === 'inscription' || item?.type === 'inscribed-sat'; + const isGalleryOpen: boolean = useMemo(() => document.documentElement.clientWidth > 360, []); const handleBackButtonClick = () => { // only go back if there is history @@ -278,191 +44,6 @@ function RareSatsDetailScreen() { setSelectedSatBundleItemIndex(null); }; - const openInGalleryView = async () => { - await chrome.tabs.create({ - url: chrome.runtime.getURL('options.html#/nft-dashboard/rare-sats-detail'), - }); - }; - - const showAlert = () => { - setshowSendOridnalsAlert(true); - }; - - const onCloseAlert = () => { - setshowSendOridnalsAlert(false); - }; - - const handleSendRareSats = async () => { - if (isPending) { - return showAlert(); - } - - if (isLedgerAccount(selectedAccount) && !isInOptions()) { - await chrome.tabs.create({ - url: chrome.runtime.getURL('options.html#/nft-dashboard/send-rare-sat'), - }); - return; - } - - navigate('/nft-dashboard/send-rare-sat'); - }; - - const handleRedirectToTx = () => { - if (pendingTxHash) { - window.open(getBtcTxStatusUrl(pendingTxHash, network), '_blank', 'noopener,noreferrer'); - } - }; - - const handleRarityScale = () => { - navigate('/nft-dashboard/supported-rarity-scale'); - }; - - const openInOrdinalsExplorer = () => { - if (!isInscription) { - return; - } - window.open(`${XVERSE_ORDIVIEW_URL(network.type)}/inscription/${item.inscription.id}`); - }; - - const { color, backgroundColor } = getRareSatsColorsByRareSatsType(item.rarity_ranking); - - const satsRanking = ( - - - - {getRareSatsLabelByType(item.rarity_ranking)} - - - } - /> - ); - const satsValue = ( - - ); - const satsRarity = ( - - ); - const ownedBy = ( - - ); - const id = ( - - ); - const title = getBundleItemId(bundle, itemIndex); - const sendActionSection = isBundle ? ( - <> - - } - text={t('COMMON.SEND')} - onPress={handleSendRareSats} - /> - - - {t('RARE_SATS.RARITY_LINK_TEXT')} - - - - ) : ( - - ); - - const extensionView = ( - - - {getBundleItemSubText({ satType: item.type, rareSatsType: item.rarity_ranking })} - - {title} - - - - - {sendActionSection} - - - - {!isUnknown && {satsRanking}} - {isBundle ? satsValue : satsRarity} - - - {!isUnknown && isBundle && {satsRarity}} - {isUnknown ? id : ownedBy} - - - {isInscription && ( - - - - )} - - ); - - const galleryView = ( - - - - - - {getBundleItemSubText({ satType: item.type, rareSatsType: item.rarity_ranking })} - - {title} - {sendActionSection} - - - - - - {t('NFT_DETAIL_SCREEN.DESCRIPTION')} - - - {!isUnknown && {satsRanking}} - {isBundle ? satsValue : satsRarity} - - - {!isUnknown && isBundle && {satsRarity}} - {isUnknown ? id : ownedBy} - - - {isInscription && ( - - - - )} - - - - ); - return ( <> {isGalleryOpen ? ( @@ -471,16 +52,7 @@ function RareSatsDetailScreen() { )} - {showSendOridnalsAlert && ( - - )} - {isGalleryOpen && selectedSatBundle !== null ? galleryView : extensionView} + TODO {!isGalleryOpen && ( diff --git a/src/app/screens/sendOrdinal/index.tsx b/src/app/screens/sendOrdinal/index.tsx index db42c9ca4..1fceae6c6 100644 --- a/src/app/screens/sendOrdinal/index.tsx +++ b/src/app/screens/sendOrdinal/index.tsx @@ -155,7 +155,7 @@ function SendOrdinal() { useEffect(() => { if (data) { - navigate(`/confirm-ordinal-tx/${selectedOrdinal?.id}`, { + navigate(`/nft-dashboard/confirm-ordinal-tx/${selectedOrdinal?.id}`, { state: { signedTxHex: data.signedTx, recipientAddress, diff --git a/src/app/screens/sendRareSat/index.tsx b/src/app/screens/sendRareSat/index.tsx index 471cab01d..c8c20d76d 100644 --- a/src/app/screens/sendRareSat/index.tsx +++ b/src/app/screens/sendRareSat/index.tsx @@ -1,9 +1,4 @@ -import ArrowLeft from '@assets/img/dashboard/arrow_left.svg'; -import AccountHeaderComponent from '@components/accountHeader'; -import BundleAsset from '@components/bundleAsset/bundleAsset'; -import SendForm from '@components/sendForm'; -import BottomBar from '@components/tabBar'; -import TopRow from '@components/topRow'; +import ActionButton from '@components/button'; import useNftDataSelector from '@hooks/stores/useNftDataSelector'; import useBtcClient from '@hooks/useBtcClient'; import { useResetUserFlow } from '@hooks/useResetUserFlow'; @@ -19,85 +14,91 @@ import { validateBtcAddress, } from '@secretkeylabs/xverse-core'; import { useMutation } from '@tanstack/react-query'; -import { StyledHeading, StyledP } from '@ui-library/common.styled'; -import { isLedgerAccount } from '@utils/helper'; -import { getBundleId, getBundleSubText } from '@utils/rareSats'; +import Callout from '@ui-library/callout'; +import { StyledHeading } from '@ui-library/common.styled'; +import InputFeedback, { InputFeedbackProps, isDangerFeedback } from '@ui-library/inputFeedback'; import BigNumber from 'bignumber.js'; -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation, useNavigate } from 'react-router-dom'; import styled from 'styled-components'; +import SendLayout from '../../layouts/sendLayout'; -const ScrollContainer = styled.div` +const Container = styled.div` display: flex; - flex: 1; flex-direction: column; - overflow-y: auto; - &::-webkit-scrollbar { - display: none; - } - width: 360px; - margin: auto; + justify-content: space-between; + flex-grow: 1; `; -const Container = styled.div({ - display: 'flex', - flexDirection: 'column', - justifyContent: 'center', - alignItems: 'center', - flex: 1, -}); +const StyledSendTo = styled(StyledHeading)` + margin-bottom: ${(props) => props.theme.space.l}; +`; -const BottomBarContainer = styled.div({ - marginTop: 'auto', -}); +const NextButtonContainer = styled.div((props) => ({ + position: 'sticky', + bottom: 0, + paddingBottom: props.theme.space.s, + paddingTop: props.theme.space.s, + backgroundColor: props.theme.colors.elevation0, +})); + +const InputGroup = styled.div` + margin-top: ${(props) => props.theme.spacing(8)}px; +`; -const ButtonContainer = styled.div((props) => ({ +const Label = styled.label((props) => ({ + ...props.theme.typography.body_medium_m, + color: props.theme.colors.white_200, display: 'flex', - flexDirection: 'row', - marginLeft: '15%', - marginTop: props.theme.spacing(40), + flex: 1, })); -const Button = styled.button((props) => ({ +const AmountInputContainer = styled.div<{ error: boolean }>((props) => ({ display: 'flex', flexDirection: 'row', - justifyContent: 'flex-end', alignItems: 'center', + marginTop: props.theme.space.xs, + marginBottom: props.theme.space.xs, + border: props.error + ? `1px solid ${props.theme.colors.danger_dark_200}` + : `1px solid ${props.theme.colors.white_800}`, + backgroundColor: props.theme.colors.elevation_n1, borderRadius: props.theme.radius(1), - backgroundColor: 'transparent', - opacity: 0.8, - marginTop: props.theme.spacing(5), + paddingLeft: props.theme.space.s, + paddingRight: props.theme.space.s, + height: 44, })); -const ButtonText = styled.div((props) => ({ - ...props.theme.body_xs, - fontWeight: 400, - fontSize: 14, - color: props.theme.colors.white['0'], - textAlign: 'center', +const InputFieldContainer = styled.div(() => ({ + flex: 1, })); -const ButtonImage = styled.img((props) => ({ - marginRight: props.theme.spacing(3), - alignSelf: 'center', - transform: 'all', +const InputField = styled.input((props) => ({ + ...props.theme.typography.body_m, + backgroundColor: 'transparent', + color: props.theme.colors.white_0, + width: '100%', + border: 'transparent', })); -const BundleAssetContainer = styled.div((props) => ({ - maxHeight: 148, - width: 148, +const ErrorContainer = styled.div((props) => ({ + marginTop: props.theme.space.xs, + marginBottom: props.theme.space.l, +})); + +const RowContainer = styled.div({ display: 'flex', - aspectRatio: 1, - justifyContent: 'center', + flexDirection: 'row', alignItems: 'center', - borderRadius: 8, - marginTop: props.theme.spacing(8), - marginBottom: props.theme.spacing(6), -})); +}); + +const StyledCallout = styled(Callout)` + margin-bottom: ${(props) => props.theme.spacing(14)}px; +`; function SendOrdinal() { - const { t } = useTranslation('translation'); + const { t } = useTranslation('translation', { keyPrefix: 'SEND' }); const navigate = useNavigate(); const { selectedSatBundle } = useNftDataSelector(); const btcClient = useBtcClient(); @@ -106,52 +107,42 @@ function SendOrdinal() { useWalletSelector(); const { getSeed } = useSeedVault(); const [ordinalUtxo, setOrdinalUtxo] = useState(undefined); - const [error, setError] = useState(''); const [recipientAddress, setRecipientAddress] = useState(''); - const [warning, setWarning] = useState(''); + const [recipientError, setRecipientError] = useState(null); useResetUserFlow('/send-rare-sat'); - const address: string | undefined = useMemo( - () => (location.state ? location.state.recipientAddress : undefined), - [location.state], - ); - const isGalleryOpen: boolean = useMemo(() => document.documentElement.clientWidth > 360, []); - - const signTransaction = async (recipient: string) => { - const addressUtxos = await btcClient.getUnspentUtxos(ordinalsAddress); - const ordUtxo = addressUtxos.find( - (utxo) => - `${utxo.txid}:${utxo.vout}` === `${selectedSatBundle?.txid}:${selectedSatBundle?.vout}`, - ); - setOrdinalUtxo(ordUtxo); - if (ordUtxo) { - const seedPhrase = await getSeed(); - const signedTx = await signOrdinalSendTransaction( - recipient, - ordUtxo, - btcAddress, - Number(selectedAccount?.id), - seedPhrase, - network.type, - [ordUtxo], - ); - - return signedTx; - } - }; - const { isLoading, data, error: txError, mutate, } = useMutation({ - mutationFn: signTransaction, + mutationFn: async (recipient) => { + const addressUtxos = await btcClient.getUnspentUtxos(ordinalsAddress); + const ordUtxo = addressUtxos.find( + (utxo) => + `${utxo.txid}:${utxo.vout}` === `${selectedSatBundle?.txid}:${selectedSatBundle?.vout}`, + ); + setOrdinalUtxo(ordUtxo); + if (ordUtxo) { + const seedPhrase = await getSeed(); + const signedTx = await signOrdinalSendTransaction( + recipient, + ordUtxo, + btcAddress, + Number(selectedAccount?.id), + seedPhrase, + network.type, + [ordUtxo], + ); + return signedTx; + } + }, }); useEffect(() => { if (data) { - navigate(`/confirm-ordinal-tx/${selectedSatBundle?.txid}`, { + navigate(`/nft-dashboard/confirm-ordinal-tx/${selectedSatBundle?.txid}`, { state: { signedTxHex: data.signedTx, recipientAddress, @@ -170,10 +161,13 @@ function SendOrdinal() { useEffect(() => { if (txError) { if (Number(txError) === ErrorCodes.InSufficientBalance) { - setError(t('SEND.ERRORS.INSUFFICIENT_BALANCE')); + setRecipientError({ variant: 'danger', message: t('ERRORS.INSUFFICIENT_BALANCE') }); } else if (Number(txError) === ErrorCodes.InSufficientBalanceWithTxFee) { - setError(t('SEND.ERRORS.INSUFFICIENT_BALANCE_FEES')); - } else setError(txError.toString()); + setRecipientError({ + variant: 'danger', + message: t('ERRORS.INSUFFICIENT_BALANCE_FEES'), + }); + } else setRecipientError({ variant: 'danger', message: txError.toString() }); } }, [txError]); // eslint-disable-line react-hooks/exhaustive-deps @@ -181,85 +175,84 @@ function SendOrdinal() { navigate(-1); }; - function validateFields(associatedAddress: string): boolean { - if (!associatedAddress) { - setError(t('SEND.ERRORS.ADDRESS_REQUIRED')); + const validateRecipientAddress = (address: string): boolean => { + if (!address) { + setRecipientError({ variant: 'danger', message: t('ERRORS.ADDRESS_REQUIRED') }); return false; } - - if (!validateBtcAddress({ btcAddress: associatedAddress, network: network.type })) { - setError(t('SEND.ERRORS.ADDRESS_INVALID')); + if ( + !validateBtcAddress({ + btcAddress: address, + network: network.type, + }) + ) { + setRecipientError({ variant: 'danger', message: t('ERRORS.ADDRESS_INVALID') }); return false; } - + if (address === ordinalsAddress || address === btcAddress) { + setRecipientError({ variant: 'info', message: t('YOU_ARE_TRANSFERRING_TO_YOURSELF') }); + return true; + } + setRecipientError(null); return true; - } + }; - const onPressNext = async (associatedAddress: string) => { - setRecipientAddress(associatedAddress); - if (validateFields(associatedAddress)) { - mutate(associatedAddress); + const onPressNext = async () => { + if (validateRecipientAddress(recipientAddress)) { + mutate(recipientAddress); } }; - const handleInputChange = (inputAddress: string) => { - if (inputAddress === ordinalsAddress) { - return setWarning(t('SEND.YOU_ARE_TRANSFERRING_TO_YOURSELF')); - } - setWarning(''); + const handleAddressChange = (e: React.ChangeEvent) => { + validateRecipientAddress(e.target.value); + setRecipientAddress(e.target.value); }; - const heading = selectedSatBundle ? getBundleSubText(selectedSatBundle) : ''; - const subText = selectedSatBundle ? getBundleId(selectedSatBundle) : ''; + const isNextEnabled = !isDangerFeedback(recipientError) && !!recipientAddress; + + // hide back button if there is no history + const hideBackButton = location.key === 'default'; return ( - <> - {isGalleryOpen && ( - <> - - {!isLedgerAccount(selectedAccount) && ( - - - - )} - - )} - - {!isGalleryOpen && } - - - - - - - {heading} - - - {subText} - - - - {!isGalleryOpen && } - - + + +
+ + {t('SEND_TO')} + + + + + + + + + + + + {recipientError && } + + + +
+ + + +
+
); } diff --git a/src/app/screens/signBatchPsbtRequest/bundleItemsComponent.tsx b/src/app/screens/signBatchPsbtRequest/bundleItemsComponent.tsx deleted file mode 100644 index 9e1f4850c..000000000 --- a/src/app/screens/signBatchPsbtRequest/bundleItemsComponent.tsx +++ /dev/null @@ -1,233 +0,0 @@ -import Eye from '@assets/img/createPassword/Eye.svg'; -import Cross from '@assets/img/dashboard/X.svg'; -import IconOrdinal from '@assets/img/transactions/ordinal.svg'; -import RareSatAsset from '@components/rareSatAsset/rareSatAsset'; -import { animated, useSpring } from '@react-spring/web'; -import { getTruncatedAddress } from '@utils/helper'; -import { BundleItem, getBundleItemSubText } from '@utils/rareSats'; -import { useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import styled from 'styled-components'; - -const Container = styled.div((props) => ({ - display: 'flex', - flexDirection: 'column', - background: props.theme.colors.elevation1, - borderRadius: 12, - padding: '16px 16px', - justifyContent: 'center', - marginBottom: 12, -})); - -const RecipientTitleText = styled.h1((props) => ({ - ...props.theme.body_medium_m, - color: props.theme.colors.white_200, - marginBottom: 10, -})); - -const RowContainer = styled.div({ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', -}); - -const TransparentButton = styled.button({ - background: 'transparent', - display: 'flex', - alignItems: 'center', - marginLeft: 10, -}); - -const Icon = styled.img((props) => ({ - marginRight: props.theme.spacing(4), - width: 32, - height: 32, - borderRadius: 30, -})); - -const TitleText = styled.h1((props) => ({ - ...props.theme.body_medium_m, - color: props.theme.colors.white_200, -})); - -const ValueText = styled.h1((props) => ({ - ...props.theme.body_medium_m, - color: props.theme.colors.white_0, -})); - -const SubValueText = styled.h1((props) => ({ - ...props.theme.body_m, - fontSize: 12, - color: props.theme.colors.white_400, -})); - -const InscriptionText = styled.h1((props) => ({ - ...props.theme.body_medium_m, - fontSize: 21, - marginTop: 24, - textAlign: 'center', - color: props.theme.colors.white[0], - overflowWrap: 'break-word', - wordWrap: 'break-word', - wordBreak: 'break-word', -})); - -const ColumnContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: 'column', - flex: 1, - justifyContent: 'flex-end', - alignItems: 'flex-end', - marginTop: props.theme.spacing(8), -})); - -const CrossContainer = styled.div({ - display: 'flex', - marginTop: 10, - justifyContent: 'flex-end', - alignItems: 'flex-end', -}); - -const OrdinalOuterImageContainer = styled.div({ - justifyContent: 'center', - alignItems: 'center', - borderRadius: 2, - display: 'flex', - flexDirection: 'column', - flex: 1, -}); - -const OrdinalImageContainer = styled.div({ - width: '50%', -}); - -const OrdinalBackgroundContainer = styled(animated.div)({ - width: '100%', - height: '100%', - top: 0, - left: 0, - bottom: 0, - right: 0, - position: 'fixed', - zIndex: 10, - background: 'rgba(18, 21, 30, 0.8)', - backdropFilter: 'blur(16px)', - padding: 16, - display: 'flex', - flexDirection: 'column', -}); - -const EyeIcon = styled.img({ - width: 20, - height: 20, -}); - -interface Props { - items: BundleItem[]; - userReceivesOrdinal?: boolean; -} -function BundleItemsComponent({ items, userReceivesOrdinal = false }: Props) { - const { t } = useTranslation('translation'); - const [showOrdinal, setShowOrdinal] = useState(false); - const [chosenOrdinal, setChosenOrdinal] = useState(0); - const styles = useSpring({ - from: { - opacity: 0, - y: 24, - }, - to: { - y: 0, - opacity: 1, - }, - delay: 100, - }); - - const onCloseClick = () => { - setShowOrdinal(false); - }; - - const getItemId = (item) => { - if (item.type === 'inscription') { - return item.inscription.id; - } - if (item.type === 'inscribed-sat' || item.type === 'rare-sat') { - return item.number; - } - return ''; - }; - - const getDetail = (item) => { - if (item.type === 'inscription' || item.type === 'inscribed-sat') { - return item.inscription.content_type; - } - - return getBundleItemSubText({ - satType: item.type, - rareSatsType: item.rarity_ranking, - }); - }; - - const getTitle = (item) => { - if (item.type === 'inscription') { - return t('COMMON.INSCRIPTION'); - } - if (item.type === 'inscribed-sat') { - return t('RARE_SATS.INSCRIBED_SAT'); - } - return t('RARE_SATS.RARE_SAT'); - }; - - return ( - <> - - - {userReceivesOrdinal - ? t('CONFIRM_TRANSACTION.YOU_WILL_RECEIVE_IN_TOTAL') - : t('CONFIRM_TRANSACTION.YOU_WILL_TRANSFER_IN_TOTAL')} - - - {items.map((item, index) => ( - // eslint-disable-next-line react/no-array-index-key - - - {getTitle(item)} - - - {getTruncatedAddress(String(getItemId(item)))} - { - setChosenOrdinal(index); - setShowOrdinal(true); - }} - > - - - - {getDetail(item)} - - - ))} - - - {showOrdinal && ( - - - - cross - - - - - - - {`${getTitle(items[chosenOrdinal])} ${getItemId( - items[chosenOrdinal], - )} `} - - - )} - - ); -} - -export default BundleItemsComponent; diff --git a/src/app/screens/signBatchPsbtRequest/index.tsx b/src/app/screens/signBatchPsbtRequest/index.tsx index 5108e0cd2..e546b7fe6 100644 --- a/src/app/screens/signBatchPsbtRequest/index.tsx +++ b/src/app/screens/signBatchPsbtRequest/index.tsx @@ -3,19 +3,19 @@ import { delay } from '@common/utils/ledger'; import AccountHeaderComponent from '@components/accountHeader'; import BottomModal from '@components/bottomModal'; import ActionButton from '@components/button'; +import SatsBundle from '@components/confirmBtcTransactionComponent/bundle'; import InputOutputComponent from '@components/confirmBtcTransactionComponent/inputOutputComponent'; import InfoContainer from '@components/infoContainer'; import LoadingTransactionStatus from '@components/loadingTransactionStatus'; import { ConfirmationStatus } from '@components/loadingTransactionStatus/circularSvgAnimation'; import RecipientComponent from '@components/recipientComponent'; import TransactionDetailComponent from '@components/transactionDetailComponent'; -import useDetectOrdinalInSignPsbt from '@hooks/useDetectOrdinalInSignPsbt'; +import useDetectOrdinalInSignPsbt, { InputsBundle } from '@hooks/useDetectOrdinalInSignPsbt'; import useSignBatchPsbtTx from '@hooks/useSignBatchPsbtTx'; import useWalletSelector from '@hooks/useWalletSelector'; import { ArrowLeft, ArrowRight } from '@phosphor-icons/react'; -import { parsePsbt, satsToBtc } from '@secretkeylabs/xverse-core'; +import { Bundle, parsePsbt, satsToBtc } from '@secretkeylabs/xverse-core'; import { isLedgerAccount } from '@utils/helper'; -import { BundleItem } from '@utils/rareSats'; import BigNumber from 'bignumber.js'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -23,8 +23,6 @@ import { useLocation, useNavigate } from 'react-router-dom'; import { MoonLoader } from 'react-spinners'; import { SignMultiplePsbtPayload } from 'sats-connect'; import styled from 'styled-components'; -import BundleItemComponent from '../signPsbtRequest/bundleItemsComponent'; -import BundleItemsComponent from './bundleItemsComponent'; const OuterContainer = styled.div` display: flex; @@ -133,7 +131,7 @@ function SignBatchPsbtRequest() { const tabId = params.get('tabId') ?? '0'; const handleOrdinalAndOrdinalInfo = useDetectOrdinalInSignPsbt(); const [userReceivesOrdinalArr, setUserReceivesOrdinalArr] = useState< - { bundleItemsData: BundleItem[]; userReceivesOrdinal: boolean }[] + { bundleItemsData: InputsBundle; userReceivesOrdinal: boolean }[] >([]); const [isLoading, setIsLoading] = useState(true); @@ -366,13 +364,17 @@ function SignBatchPsbtRequest() { - {userTransfersOrdinals.length > 0 && ( - - )} + {userTransfersOrdinals.length > 0 && + userTransfersOrdinals.map((item, index) => ( + // eslint-disable-next-line react/no-array-index-key + + ))} - {userReceivesOrdinals.length > 0 && ( - - )} + {userReceivesOrdinals.length > 0 && + userTransfersOrdinals.map((item, index) => ( + // eslint-disable-next-line react/no-array-index-key + + ))} - {userReceivesOrdinalArr[currentPsbtIndex]?.bundleItemsData?.map((bundleItem, index) => ( - - ))} + )} ({ - display: 'flex', - flexDirection: 'column', - background: props.theme.colors.elevation1, - borderRadius: 12, - padding: '16px 16px', - justifyContent: 'center', - marginBottom: 12, -})); - -const RecipientTitleText = styled.h1((props) => ({ - ...props.theme.body_medium_m, - color: props.theme.colors.white_200, - marginBottom: 10, -})); - -const RowContainer = styled.div({ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', -}); - -const TransparentButton = styled.button({ - background: 'transparent', - display: 'flex', - alignItems: 'center', - marginLeft: 10, -}); - -const Icon = styled.img((props) => ({ - marginRight: props.theme.spacing(4), - width: 32, - height: 32, - borderRadius: 30, -})); - -const TitleText = styled.h1((props) => ({ - ...props.theme.body_medium_m, - color: props.theme.colors.white_200, -})); - -const ValueText = styled.h1((props) => ({ - ...props.theme.body_medium_m, - color: props.theme.colors.white_0, -})); - -const SubValueText = styled.h1((props) => ({ - ...props.theme.body_m, - fontSize: 12, - color: props.theme.colors.white_400, -})); - -const InscriptionText = styled.h1((props) => ({ - ...props.theme.body_medium_m, - fontSize: 21, - marginTop: 24, - textAlign: 'center', - color: props.theme.colors.white[0], - overflowWrap: 'break-word', - wordWrap: 'break-word', - wordBreak: 'break-word', -})); - -const ColumnContainer = styled.div({ - display: 'flex', - flexDirection: 'column', - flex: 1, - justifyContent: 'flex-end', - alignItems: 'flex-end', - marginTop: 12, -}); - -const CrossContainer = styled.div({ - display: 'flex', - marginTop: 10, - justifyContent: 'flex-end', - alignItems: 'flex-end', -}); - -const OrdinalOuterImageContainer = styled.div({ - justifyContent: 'center', - alignItems: 'center', - borderRadius: 2, - display: 'flex', - flexDirection: 'column', - flex: 1, -}); - -const OrdinalImageContainer = styled.div({ - width: '50%', -}); - -const OrdinalBackgroundContainer = styled(animated.div)({ - width: '100%', - height: '100%', - top: 0, - left: 0, - bottom: 0, - right: 0, - position: 'fixed', - zIndex: 10, - background: 'rgba(18, 21, 30, 0.8)', - backdropFilter: 'blur(16px)', - padding: 16, - display: 'flex', - flexDirection: 'column', -}); - -const EyeIcon = styled.img({ - width: 20, - height: 20, -}); - -interface Props { - item: BundleItem; - userReceivesOrdinal: boolean; -} -function BundleItemsComponent({ item, userReceivesOrdinal }: Props) { - const { t } = useTranslation('translation'); - const [showOrdinal, setShowOrdinal] = useState(false); - const styles = useSpring({ - from: { - opacity: 0, - y: 24, - }, - to: { - y: 0, - opacity: 1, - }, - delay: 100, - }); - const onButtonClick = () => { - setShowOrdinal(true); - }; - - const onCrossClick = () => { - setShowOrdinal(false); - }; - const getItemId = () => { - if (item.type === 'inscription') { - return item.inscription.id; - } - if (item.type === 'inscribed-sat' || item.type === 'rare-sat') { - return item.number; - } - return ''; - }; - const itemSubText = getBundleItemSubText({ - satType: item.type, - rareSatsType: item.rarity_ranking, - }); - const getDetail = () => { - if (item.type === 'inscription' || item.type === 'inscribed-sat') { - return item.inscription.content_type; - } - return itemSubText; - }; - const getTitle = () => { - if (item.type === 'inscription') { - return t('COMMON.INSCRIPTION'); - } - if (item.type === 'inscribed-sat') { - return t('RARE_SATS.INSCRIBED_SAT'); - } - return t('RARE_SATS.RARE_SAT'); - }; - return ( - <> - {showOrdinal && ( - - - - cross - - - - - - - {`${getTitle()} ${getItemId()} `} - - - )} - - - {userReceivesOrdinal - ? t('CONFIRM_TRANSACTION.YOU_WILL_RECEIVE') - : t('CONFIRM_TRANSACTION.YOU_WILL_TRANSFER')} - - - - {getTitle()} - - - {getTruncatedAddress(String(getItemId()))} - - - - - {getDetail()} - - - - - ); -} - -export default BundleItemsComponent; diff --git a/src/app/screens/signPsbtRequest/index.tsx b/src/app/screens/signPsbtRequest/index.tsx index ed52dbe5e..0e87e1542 100644 --- a/src/app/screens/signPsbtRequest/index.tsx +++ b/src/app/screens/signPsbtRequest/index.tsx @@ -5,17 +5,19 @@ import { delay } from '@common/utils/ledger'; import AccountHeaderComponent from '@components/accountHeader'; import BottomModal from '@components/bottomModal'; import ActionButton from '@components/button'; +import SatsBundle from '@components/confirmBtcTransactionComponent/bundle'; import InputOutputComponent from '@components/confirmBtcTransactionComponent/inputOutputComponent'; import InfoContainer from '@components/infoContainer'; import LedgerConnectionView from '@components/ledger/connectLedgerView'; import RecipientComponent from '@components/recipientComponent'; import TransactionDetailComponent from '@components/transactionDetailComponent'; import useBtcClient from '@hooks/useBtcClient'; -import useDetectOrdinalInSignPsbt from '@hooks/useDetectOrdinalInSignPsbt'; +import useDetectOrdinalInSignPsbt, { InputsBundle } from '@hooks/useDetectOrdinalInSignPsbt'; import useSignPsbtTx from '@hooks/useSignPsbtTx'; import useWalletSelector from '@hooks/useWalletSelector'; import Transport from '@ledgerhq/hw-transport-webusb'; import { + Bundle, getBtcFiatEquivalent, parsePsbt, psbtBase64ToHex, @@ -24,7 +26,6 @@ import { Transport as TransportType, } from '@secretkeylabs/xverse-core'; import { isLedgerAccount } from '@utils/helper'; -import { BundleItem } from '@utils/rareSats'; import BigNumber from 'bignumber.js'; import { decodeToken } from 'jsontokens'; import { useEffect, useMemo, useState } from 'react'; @@ -34,7 +35,6 @@ import { useLocation, useNavigate } from 'react-router-dom'; import { MoonLoader } from 'react-spinners'; import { SignTransactionOptions } from 'sats-connect'; import styled from 'styled-components'; -import BundleItemsComponent from './bundleItemsComponent'; const OuterContainer = styled.div` display: flex; @@ -110,6 +110,9 @@ function SignPsbtRequest() { const { t: signatureRequestTranslate } = useTranslation('translation', { keyPrefix: 'SIGNATURE_REQUEST', }); + const { t: rareSatsTranslate } = useTranslation('translation', { + keyPrefix: 'RARE_SATS', + }); const [expandInputOutputView, setExpandInputOutputView] = useState(false); const { payload, confirmSignPsbt, cancelSignPsbt, getSigningAddresses } = useSignPsbtTx(); const [isSigning, setIsSigning] = useState(false); @@ -139,7 +142,7 @@ function SignPsbtRequest() { const handleOrdinalAndOrdinalInfo = useDetectOrdinalInSignPsbt(); const [isLoading, setIsLoading] = useState(true); const [userReceivesOrdinal, setUserReceivesOrdinal] = useState(false); - const [bundleItemsData, setBundleItemsData] = useState([]); + const [bundleItemsData, setBundleItemsData] = useState(); const signingAddresses = useMemo( () => getSigningAddresses(payload.inputsToSign), [payload.inputsToSign], @@ -384,15 +387,12 @@ function SignPsbtRequest() { {t('REVIEW_TRANSACTION')} {!payload.broadcast && } - {bundleItemsData && - bundleItemsData.map((bundleItem, index) => ( - - ))} + {bundleItemsData && ( + + )} + {payload.broadcast ? ( `https://ord${network === 'Mainnet' ? '' : '-testnet'}.xverse.app`; -export const MAGISAT_IO_RARITY_SCAN_URL = 'https://magisat.io/wallet?walletAddress='; - export const TRANSAC_URL = 'https://global.transak.com'; export const TRANSAC_API_KEY = process.env.TRANSAC_API_KEY; export const MOON_PAY_URL = 'https://buy.moonpay.com'; diff --git a/src/app/utils/inscriptions.ts b/src/app/utils/inscriptions.ts index 2afc4b87c..4d3424186 100644 --- a/src/app/utils/inscriptions.ts +++ b/src/app/utils/inscriptions.ts @@ -1,11 +1,9 @@ import { - CondensedInscription, Inscription, InscriptionCollectionsData, isBrcTransferValid, } from '@secretkeylabs/xverse-core'; import type { Color } from 'theme'; -import { BundleItem } from './rareSats'; export type Brc20Status = 'valid' | 'used'; @@ -45,11 +43,3 @@ export const getInscriptionsTabGridItemSubText = (collection: InscriptionCollect } return collection.total_inscriptions > 1 ? `${collection.total_inscriptions} Items` : '1 Item'; }; - -export const mapCondensedInscriptionToBundleItem = ( - inscription: CondensedInscription, -): BundleItem => ({ - inscription, - type: 'inscription', - rarity_ranking: 'common', // TODO eventually want to fetch this rarity and display it -}); diff --git a/src/app/utils/rareSats.test.ts b/src/app/utils/rareSats.test.ts deleted file mode 100644 index 304a814ae..000000000 --- a/src/app/utils/rareSats.test.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { describe, expect, test } from 'vitest'; -import { ApiBundle, Bundle, mapRareSatsAPIResponseToRareSats } from './rareSats'; - -describe('rareSats', () => { - describe('mapRareSatsAPIResponseToRareSats', () => { - const testCases: Array<{ name: string; input: ApiBundle; expected: Bundle }> = [ - { - name: 'mixed (sats, inscriptions)', - input: { - txid: '10f78695a5f83dc2c508fffceb479e49423cf5d538c680864e56c0020c7f262e', - vout: 0, - block_height: 803440, - value: 600, - sats: [ - { - number: '32234503563456', - offset: 0, - rarity_ranking: 'epic', - }, - { - number: '0', - offset: 100, - rarity_ranking: 'mythic', - }, - ], - inscriptions: [ - { - id: '10f78695a5f83dc2c508fffceb479e49423cf5d538c680864e56c0020c7f262ei0', - offset: 0, - content_type: 'image/jpeg', - }, - { - id: '10f78695a5f83dc2c508fffceb479e49423cf5d538c680864e56c0020c7f262ei1', - offset: 500, - content_type: 'text/html', - }, - ], - }, - expected: { - txid: '10f78695a5f83dc2c508fffceb479e49423cf5d538c680864e56c0020c7f262e', - vout: 0, - block_height: 803440, - value: 600, - items: [ - { - inscription: { - content_type: 'text/html', - id: '10f78695a5f83dc2c508fffceb479e49423cf5d538c680864e56c0020c7f262ei1', - }, - rarity_ranking: 'common', - type: 'inscription', - }, - { - inscription: { - content_type: 'image/jpeg', - id: '10f78695a5f83dc2c508fffceb479e49423cf5d538c680864e56c0020c7f262ei0', - }, - rarity_ranking: 'epic', - type: 'inscribed-sat', - number: '32234503563456', - }, - { - number: '0', - rarity_ranking: 'mythic', - type: 'rare-sat', - }, - ], - }, - }, - { - name: 'only rare sats', - input: { - txid: '10f78695a5f83dc2c508fffceb479e49423cf5d538c680864e56c0020c7f262e', - vout: 0, - block_height: 803440, - value: 600, - sats: [ - { - number: '32234503563456', - offset: 0, - rarity_ranking: 'epic', - }, - { - number: '0', - offset: 100, - rarity_ranking: 'mythic', - }, - ], - inscriptions: [], - }, - expected: { - txid: '10f78695a5f83dc2c508fffceb479e49423cf5d538c680864e56c0020c7f262e', - vout: 0, - block_height: 803440, - value: 600, - items: [ - { - number: '32234503563456', - rarity_ranking: 'epic', - type: 'rare-sat', - }, - { - number: '0', - rarity_ranking: 'mythic', - type: 'rare-sat', - }, - ], - }, - }, - { - name: 'only inscriptions', - input: { - txid: '10f78695a5f83dc2c508fffceb479e49423cf5d538c680864e56c0020c7f262e', - vout: 0, - block_height: 803440, - value: 600, - sats: [], - inscriptions: [ - { - id: '10f78695a5f83dc2c508fffceb479e49423cf5d538c680864e56c0020c7f262ei0', - offset: 0, - content_type: 'image/jpeg', - }, - { - id: '10f78695a5f83dc2c508fffceb479e49423cf5d538c680864e56c0020c7f262ei1', - offset: 500, - content_type: 'text/html', - }, - ], - }, - expected: { - txid: '10f78695a5f83dc2c508fffceb479e49423cf5d538c680864e56c0020c7f262e', - vout: 0, - block_height: 803440, - value: 600, - items: [ - { - inscription: { - content_type: 'image/jpeg', - id: '10f78695a5f83dc2c508fffceb479e49423cf5d538c680864e56c0020c7f262ei0', - }, - rarity_ranking: 'common', - type: 'inscription', - }, - { - inscription: { - content_type: 'text/html', - id: '10f78695a5f83dc2c508fffceb479e49423cf5d538c680864e56c0020c7f262ei1', - }, - rarity_ranking: 'common', - type: 'inscription', - }, - ], - }, - }, - { - name: 'unknown', - input: { - txid: '10f78695a5f83dc2c508fffceb479e49423cf5d538c680864e56c0020c7f262e', - vout: 0, - block_height: 803440, - value: 600, - sats: [], - inscriptions: [], - }, - expected: { - txid: '10f78695a5f83dc2c508fffceb479e49423cf5d538c680864e56c0020c7f262e', - vout: 0, - block_height: 803440, - value: 600, - items: [ - { - type: 'unknown', - rarity_ranking: 'unknown', - }, - ], - }, - }, - ]; - - testCases.forEach(({ name, input, expected }) => { - test(name, () => { - expect(mapRareSatsAPIResponseToRareSats(input)).toEqual(expected); - }); - }); - }); -}); diff --git a/src/app/utils/rareSats.ts b/src/app/utils/rareSats.ts index d7217e96c..1506977ed 100644 --- a/src/app/utils/rareSats.ts +++ b/src/app/utils/rareSats.ts @@ -1,234 +1,43 @@ +import { + Bundle, + RareSatsType, + RodarmorRareSats, + RodarmorRareSatsType, +} from '@secretkeylabs/xverse-core'; import { t } from 'i18next'; import { getTruncatedAddress } from './helper'; -const RoadArmorRareSats = ['uncommon', 'rare', 'epic', 'legendary', 'mythic', 'common'] as const; -export type RoadArmorRareSatsType = (typeof RoadArmorRareSats)[number]; +export const getRareSatsLabelByType = (type: RareSatsType) => t(`RARE_SATS.RARITY_LABEL.${type}`); -export const RareSats = ['unknown', ...RoadArmorRareSats] as const; -export type RareSatsType = (typeof RareSats)[number]; - -export const getRareSatsLabelByType = (type: RareSatsType) => - t(`RARE_SATS.RARE_TYPES.${type.toUpperCase()}`); - -export type SatType = 'inscription' | 'rare-sat' | 'inscribed-sat' | 'unknown'; - -export const getBundleItemSubText = ({ - satType, - rareSatsType, -}: { - satType: SatType; - rareSatsType?: RareSatsType; -}) => - ({ - inscription: t('COMMON.INSCRIPTION'), - 'rare-sat': t('RARE_SATS.SAT_TYPES.RARE_SAT', { - type: getRareSatsLabelByType(rareSatsType ?? 'unknown'), - }), - 'inscribed-sat': t('RARE_SATS.SAT_TYPES.INSCRIBED_RARE_SAT', { - type: getRareSatsLabelByType(rareSatsType ?? 'unknown'), - }), - unknown: t('RARE_SATS.SAT_TYPES.UNKNOWN_RARE_SAT'), - }[satType]); - -// TODO: make number separator dynamic by locale, extension only supports en-US for now so this is not a priority -export const getRarityLabelByRareSatsType = (rareSatsType: RareSatsType) => - ({ - mythic: t('RARE_SATS.RARITY_RANKING_POSITION', { position: '1 / 2.1' }), - legendary: t('RARE_SATS.RARITY_RANKING_POSITION', { position: '5 / 2.1' }), - epic: t('RARE_SATS.RARITY_RANKING_POSITION', { position: '32 / 2.1' }), - rare: t('RARE_SATS.RARITY_RANKING_POSITION', { position: '3,437 / 2.1' }), - uncommon: t('RARE_SATS.RARITY_RANKING_POSITION', { position: '6,929,999 / 2.1' }), - common: '--', - }[rareSatsType]); - -export const getRareSatsColorsByRareSatsType = (rareSatsType: RareSatsType) => - ({ - unknown: { - color: 'rgb(175,186,189)', - backgroundColor: 'rgba(175,186,189,0.15)', - }, - uncommon: { - color: 'rgb(0,218,182)', - backgroundColor: 'rgba(0,218,182,0.15)', - }, - rare: { - color: 'rgb(100,196,246)', - backgroundColor: 'rgba(100,196,246,0.15)', - }, - epic: { - color: 'rgb(182,105,254)', - backgroundColor: 'rgba(182,105,254,0.15)', - }, - legendary: { - color: 'rgb(255,205,120)', - backgroundColor: 'rgba(255,205,120,0.15)', - }, - mythic: { - color: 'rgb(255,244,203)', - backgroundColor: 'rgba(255,244,203, 0.15)', - }, - common: { - color: 'rgb(216,216,216)', - backgroundColor: 'rgba(216,216,216,0.15)', - }, - }[rareSatsType ?? 'common']); - -type SatInscription = { - id: string; - offset: number; - content_type: string; -}; - -type Sat = { number: string; offset: number; rarity_ranking: RoadArmorRareSatsType }; - -export type ApiBundle = { - txid: string; - vout: number; - block_height: number; - value: number; - sats: Array; - inscriptions: Array; -}; - -export type BundleItem = - | { - type: 'rare-sat'; - rarity_ranking: RoadArmorRareSatsType; - number: string; - } - | { - type: 'inscribed-sat'; - rarity_ranking: RoadArmorRareSatsType; - number: string; - inscription: { - id: string; - content_type: string; - }; - } - | { - type: 'inscription'; - rarity_ranking: RoadArmorRareSatsType; - inscription: { - id: string; - content_type: string; - }; - } - | { - type: 'unknown'; - rarity_ranking: 'unknown'; - }; - -export type Bundle = Omit & { - items: Array; -}; - -export const mapRareSatsAPIResponseToRareSats = (apiBundles: ApiBundle): Bundle => { - const generalBundleInfo = { - txid: apiBundles.txid, - vout: apiBundles.vout, - block_height: apiBundles.block_height, - value: apiBundles.value, - }; - - // unknown - if (!apiBundles.sats.length && !apiBundles.inscriptions.length) { - return { ...generalBundleInfo, items: [{ type: 'unknown', rarity_ranking: 'unknown' }] }; - } - - // only rare sats - if (!apiBundles.inscriptions.length) { - return { - ...generalBundleInfo, - items: apiBundles.sats.map((sat) => ({ - type: 'rare-sat', - rarity_ranking: sat.rarity_ranking, - number: sat.number, - })), - }; - } - - // can be mixed - const satsObject = apiBundles.sats.reduce((acc, sat) => { - acc[sat.offset] = sat; - return acc; - }, {} as Record); - - const inscriptionsObject: Record = {}; - const items: Array = []; - - apiBundles.inscriptions.forEach((inscription) => { - inscriptionsObject[inscription.offset] = inscription; - - if (satsObject[inscription.offset]) { - return; - } - items.push({ - type: 'inscription', - rarity_ranking: 'common', - inscription: { - id: inscription.id, - content_type: inscription.content_type, - }, - }); - }); - - apiBundles.sats.forEach((sat) => { - const inscription = inscriptionsObject[sat.offset]; - if (!inscription) { - return items.push({ - type: 'rare-sat', - rarity_ranking: sat.rarity_ranking, - number: sat.number, - }); - } - items.push({ - type: 'inscribed-sat', - rarity_ranking: sat.rarity_ranking, - number: sat.number, - inscription: { - id: inscription.id, - content_type: inscription.content_type, - }, - }); - }); - - return { - ...generalBundleInfo, - items, +export const getRareSatsColorsByRareSatsType = (rareSatsType: RareSatsType) => { + const colors: Partial> = { + UNCOMMON: 'rgba(215, 105, 254, 0.20)', + RARE: 'rgba(131, 113, 242, 0.20)', + EPIC: 'rgba(145, 226, 96, 0.20)', + LEGENDARY: 'rgba(255,205,120,0.20)', + MYTHIC: 'rgba(255,244,203, 0.20)', + COMMON: 'rgba(175,186,189,0.20)', }; + return colors[rareSatsType]; }; -const getFormattedTxIdVoutFromBundle = (bundle: Bundle) => +export const getFormattedTxIdVoutFromBundle = (bundle: Bundle) => `${getTruncatedAddress(bundle.txid, 6)}:${bundle.vout}`; -export const getBundleId = (bundle: Bundle): string => { - if ( - bundle.items.length === 1 && - bundle.items[0].type !== 'unknown' && - bundle.items[0].type !== 'inscription' - ) { - return bundle.items[0].number; +export const getSatLabel = (satributes: RareSatsType[]): string => { + const isLengthGrateThanTwo = satributes.length > 2; + if (satributes.length === 1) { + return `${getRareSatsLabelByType(satributes[0])}`; } - return getFormattedTxIdVoutFromBundle(bundle); -}; - -export const getBundleSubText = (bundle: Bundle): string => { - if (bundle.items.length > 1) { - return t('RARE_SATS.RARE_SATS_BUNDLE'); + // we expect to roadarmor sats be in the first position + if (RodarmorRareSats.includes(satributes[0] as RodarmorRareSatsType)) { + return `${getRareSatsLabelByType(satributes[0])} ${t( + isLengthGrateThanTwo ? 'COMMON.COMBO' : `RARE_SATS.RARITY_LABEL.${satributes[1]}`, + )}`; } - const item = bundle.items[0]; - return getBundleItemSubText({ satType: item.type, rareSatsType: item.rarity_ranking }); -}; - -export const getBundleItemId = (bundle: Bundle, index: number): string => { - const item = bundle.items[index]; - if (item.type === 'unknown') { - return getFormattedTxIdVoutFromBundle(bundle); - } - if (item.type === 'inscription' || item.type === 'inscribed-sat') { - return getTruncatedAddress(item.inscription.id, 6); - } - return item.number; + return isLengthGrateThanTwo + ? `${t('COMMON.COMBO')}` + : `${getRareSatsLabelByType(satributes[0])} ${getRareSatsLabelByType(satributes[1])}`; }; diff --git a/src/assets/img/nftDashboard/rareSats/1d_pali.svg b/src/assets/img/nftDashboard/rareSats/1d_pali.svg new file mode 100644 index 000000000..d8b6cceec --- /dev/null +++ b/src/assets/img/nftDashboard/rareSats/1d_pali.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/img/nftDashboard/rareSats/2d_pali.svg b/src/assets/img/nftDashboard/rareSats/2d_pali.svg new file mode 100644 index 000000000..29117d3f1 --- /dev/null +++ b/src/assets/img/nftDashboard/rareSats/2d_pali.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/img/nftDashboard/rareSats/3d_pali.svg b/src/assets/img/nftDashboard/rareSats/3d_pali.svg new file mode 100644 index 000000000..122851258 --- /dev/null +++ b/src/assets/img/nftDashboard/rareSats/3d_pali.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/img/nftDashboard/rareSats/alpha.svg b/src/assets/img/nftDashboard/rareSats/alpha.svg new file mode 100644 index 000000000..612fb5d2b --- /dev/null +++ b/src/assets/img/nftDashboard/rareSats/alpha.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/img/nftDashboard/rareSats/black_epic.svg b/src/assets/img/nftDashboard/rareSats/black_epic.svg new file mode 100644 index 000000000..fa2dc2102 --- /dev/null +++ b/src/assets/img/nftDashboard/rareSats/black_epic.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/img/nftDashboard/rareSats/black_legendary.svg b/src/assets/img/nftDashboard/rareSats/black_legendary.svg new file mode 100644 index 000000000..f5f6ba4ef --- /dev/null +++ b/src/assets/img/nftDashboard/rareSats/black_legendary.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/img/nftDashboard/rareSats/black_rare.svg b/src/assets/img/nftDashboard/rareSats/black_rare.svg new file mode 100644 index 000000000..292974198 --- /dev/null +++ b/src/assets/img/nftDashboard/rareSats/black_rare.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/assets/img/nftDashboard/rareSats/black_uncommon.svg b/src/assets/img/nftDashboard/rareSats/black_uncommon.svg new file mode 100644 index 000000000..016063156 --- /dev/null +++ b/src/assets/img/nftDashboard/rareSats/black_uncommon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/img/nftDashboard/rareSats/block_78.svg b/src/assets/img/nftDashboard/rareSats/block_78.svg new file mode 100644 index 000000000..df22e9a77 --- /dev/null +++ b/src/assets/img/nftDashboard/rareSats/block_78.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/img/nftDashboard/rareSats/block_9.svg b/src/assets/img/nftDashboard/rareSats/block_9.svg new file mode 100644 index 000000000..1ca053006 --- /dev/null +++ b/src/assets/img/nftDashboard/rareSats/block_9.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/img/nftDashboard/rareSats/block_pali.svg b/src/assets/img/nftDashboard/rareSats/block_pali.svg new file mode 100644 index 000000000..453c76441 --- /dev/null +++ b/src/assets/img/nftDashboard/rareSats/block_pali.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/img/nftDashboard/rareSats/epic.svg b/src/assets/img/nftDashboard/rareSats/epic.svg index 5efdf652e..b5267895a 100644 --- a/src/assets/img/nftDashboard/rareSats/epic.svg +++ b/src/assets/img/nftDashboard/rareSats/epic.svg @@ -1,9 +1,9 @@ - - - - - - - - + + + + + + + + diff --git a/src/assets/img/nftDashboard/rareSats/fibonacci_sequence.svg b/src/assets/img/nftDashboard/rareSats/fibonacci_sequence.svg new file mode 100644 index 000000000..20d232759 --- /dev/null +++ b/src/assets/img/nftDashboard/rareSats/fibonacci_sequence.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/img/nftDashboard/rareSats/first_transaction_silkroad.svg b/src/assets/img/nftDashboard/rareSats/first_transaction_silkroad.svg new file mode 100644 index 000000000..da992764c --- /dev/null +++ b/src/assets/img/nftDashboard/rareSats/first_transaction_silkroad.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/img/nftDashboard/rareSats/hitman.svg b/src/assets/img/nftDashboard/rareSats/hitman.svg new file mode 100644 index 000000000..544415779 --- /dev/null +++ b/src/assets/img/nftDashboard/rareSats/hitman.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/img/nftDashboard/rareSats/jpeg.svg b/src/assets/img/nftDashboard/rareSats/jpeg.svg new file mode 100644 index 000000000..537c084dc --- /dev/null +++ b/src/assets/img/nftDashboard/rareSats/jpeg.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/img/nftDashboard/rareSats/nakamoto.svg b/src/assets/img/nftDashboard/rareSats/nakamoto.svg new file mode 100644 index 000000000..82cce4dc7 --- /dev/null +++ b/src/assets/img/nftDashboard/rareSats/nakamoto.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/img/nftDashboard/rareSats/omega.svg b/src/assets/img/nftDashboard/rareSats/omega.svg new file mode 100644 index 000000000..b356cc15b --- /dev/null +++ b/src/assets/img/nftDashboard/rareSats/omega.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/img/nftDashboard/rareSats/palinception.svg b/src/assets/img/nftDashboard/rareSats/palinception.svg new file mode 100644 index 000000000..c51b51469 --- /dev/null +++ b/src/assets/img/nftDashboard/rareSats/palinception.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/img/nftDashboard/rareSats/palindrome.svg b/src/assets/img/nftDashboard/rareSats/palindrome.svg new file mode 100644 index 000000000..ac568fc3c --- /dev/null +++ b/src/assets/img/nftDashboard/rareSats/palindrome.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/src/assets/img/nftDashboard/rareSats/pizza.svg b/src/assets/img/nftDashboard/rareSats/pizza.svg new file mode 100644 index 000000000..122cc7204 --- /dev/null +++ b/src/assets/img/nftDashboard/rareSats/pizza.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/img/nftDashboard/rareSats/rare.svg b/src/assets/img/nftDashboard/rareSats/rare.svg index 452b9a55c..4b00d56d9 100644 --- a/src/assets/img/nftDashboard/rareSats/rare.svg +++ b/src/assets/img/nftDashboard/rareSats/rare.svg @@ -1,7 +1,7 @@ - - - - - - + + + + + + diff --git a/src/assets/img/nftDashboard/rareSats/sequence_pali.svg b/src/assets/img/nftDashboard/rareSats/sequence_pali.svg new file mode 100644 index 000000000..e5168f27d --- /dev/null +++ b/src/assets/img/nftDashboard/rareSats/sequence_pali.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/img/nftDashboard/rareSats/uncommon.svg b/src/assets/img/nftDashboard/rareSats/uncommon.svg index 257ee10ba..9582059b4 100644 --- a/src/assets/img/nftDashboard/rareSats/uncommon.svg +++ b/src/assets/img/nftDashboard/rareSats/uncommon.svg @@ -1,8 +1,8 @@ - - - - - - - + + + + + + + diff --git a/src/assets/img/nftDashboard/rareSats/vintage.svg b/src/assets/img/nftDashboard/rareSats/vintage.svg new file mode 100644 index 000000000..51ad3ade9 --- /dev/null +++ b/src/assets/img/nftDashboard/rareSats/vintage.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + diff --git a/src/assets/img/rareSats/satBundle.svg b/src/assets/img/rareSats/satBundle.svg new file mode 100644 index 000000000..e9941a7cb --- /dev/null +++ b/src/assets/img/rareSats/satBundle.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/locales/en.json b/src/locales/en.json index 806715d52..0835bbf82 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1,7 +1,10 @@ { "COMMON": { "INSCRIPTION": "Inscription", - "SEND": "Send" + "SEND": "Send", + "COMBO": "Combo", + "SATS": "Sats", + "SATTRIBUTES": "Sattributes" }, "LANDING_SCREEN": { "SCREEN_TITLE": "Wallet for Stacks & Bitcoin", @@ -585,19 +588,6 @@ "VERIFY_ADDRESS_ON_LEDGER": "Verify address on Ledger", "VIEW_ADDRESS": "View address", "ADD_STACKS_ADDRESS": "Add a Stacks address", - "RARITY_DETAIL": { - "TOP_TEXT": "Currently, only the default rarity scale is supported. Your sats could have other attributes.", - "LEARN_MORE": "Learn more", - "UNKNOWN": "A sat of unknown rarity.", - "UNCOMMON": "The first sat of each block.", - "RARE": "The first sat of each difficulty adjustment period.", - "EPIC": "The first sat of each halving epoch.", - "LEGENDARY": "The first sat of each cycle.", - "MYTHIC": "The first sat of the genesis block.", - "SCAN": "Scan for custom attributes", - "SUPPORTED_RARITIES": "Supported rarities", - "RARITY_INFO": "Currently, only the default rarity scale is supported. Your sats could have other attributes." - }, "NFTS": "NFTs", "RARE_SATS": "Rare Sats", "NEW_FEATURE": "New Feature, Rare Sats", @@ -608,7 +598,8 @@ "RARE_SATS_NOTICE_TITLE": "Don't see your rare sat?", "RARE_SATS_NOTICE_DETAIL": "Currently, Xverse only supports the Rodarmor rarity index. Your sats may have other attributes.", "SEE_SUPPORTED": "See supported rarity scale", - "FROM_RARE_SAT_BUNDLE": "This inscription belongs to the same bundle as other assets. Transferring it will involve transferring the full bundle." + "FROM_RARE_SAT_BUNDLE": "This inscription belongs to the same bundle as other assets. Transferring it will involve transferring the full bundle.", + "HOLDS_RARE_SAT": "This inscription holds a rare sat." }, "RESTORE_FUND_SCREEN": { "TITLE": "Restore assets", @@ -1101,14 +1092,73 @@ "LEARN_MORE": "Learn more" }, "RARE_SATS": { - "RARE_TYPES": { - "UNKNOWN": "Unknown", + "RARITY_LABEL": { "UNCOMMON": "Uncommon", - "COMMON": "Common", + "COMMON": "Common/Unknown", "RARE": "Rare", "EPIC": "Epic", "LEGENDARY": "Legendary", - "MYTHIC": "Mythic" + "MYTHIC": "Mythic", + "BLACK_LEGENDARY": "Black Legendary", + "BLACK_EPIC": "Black Epic", + "BLACK_RARE": "Black Rare", + "BLACK_UNCOMMON": "Black Uncommon", + "FIBONACCI": "Fibonacci Sequence", + "1D_PALINDROME": "1D Pali", + "2D_PALINDROME": "2D Pali", + "3D_PALINDROME": "3D Pali", + "SEQUENCE_PALINDROME": "Sequence Pali", + "PERFECT_PALINCEPTION": "Perfect Paliception", + "PALIBLOCK_PALINDROME": "Block Pali", + "PALINDROME": "Palindrome", + "NAME_PALINDROME": "Name Palindrome", + "ALPHA": "Alpha", + "OMEGA": "Omega", + "FIRST_TRANSACTION": "First transaction", + "BLOCK9": "Block 9", + "BLOCK78": "Block 78", + "NAKAMOTO": "Nakamoto", + "VINTAGE": "Vintage", + "PIZZA": "Pizza", + "JPEG": "Jpeg", + "HITMAN": "Hitman", + "SILK_ROAD": "Silkroad" + }, + "RARITY_DETAIL": { + "TOP_TEXT": "Currently, only the default rarity scale is supported. Your sats could have other attributes.", + "LEARN_MORE": "Learn more", + "RARITY_INFO": "Currently, only the default rarity scale is supported. Your sats could have other attributes.", + "SUPPORTED_RARITIES": "Supported rarities", + "MYTHIC": "The first sat of the genesis block.", + "LEGENDARY": "The first sat of each cycle.", + "EPIC": "The first sat of each halving epoch.", + "RARE": "The first sat of each difficulty adjustment period.", + "UNCOMMON": "The first sat of each block.", + "COMMON": "A sat of unknown rarity.", + "BLACK_LEGENDARY": "The last sat of each cycle.", + "BLACK_EPIC": "The last sat of each halving epoch.", + "BLACK_RARE": "The last sat of each difficulty adjustment period.", + "BLACK_UNCOMMON": "The last sat of each block.", + "FIBONACCI": "Sats with IDs that follow the Fibonacci Sequence.", + "1D_PALINDROME": "Sats with number composed of only 1 digit (ex: 888888888888).", + "2D_PALINDROME": "Sats with palindromic number, composed of only 2 digit (ex: 8888822288888).", + "3D_PALINDROME": "Sats with palindromic number, composed of only 3 digit (ex: 8885522255888).", + "SEQUENCE_PALINDROME": "Sats with palindromic number, and a sequence of at least 3 consecutive identical digits (ex: 3275433345723).", + "PERFECT_PALINCEPTION": "Sats with palindromic number made of a subsequence which is also a palindrome of at least 2 distinct digits.", + "PALIBLOCK_PALINDROME": "Sats with palindromic number, in a block with a palindromic number.", + "PALINDROME": "Sats with palindromic number (ex: 3275431345723).", + "NAME_PALINDROME": "Sats with palindromic names (ex: abcba).", + "ALPHA": "The first sats in each bitcoin. They always end in at least 8 zeros.", + "OMEGA": "The last sats in each bitcoin. They always end in at least 8 nines.", + "FIRST_TRANSACTION": "Sats from the 10 bitcoins Satoshi Nakamoto sent Hal Finney in the first bitcoin transaction.", + "BLOCK9": "Sats mined in Block 9 (the first block with sats circulating today).", + "BLOCK78": "Sats mined by Hal Finney in Block 78 (the first block mined by someone other than Satoshi).", + "NAKAMOTO": "Sats mined by Satoshi Nakamoto himself.", + "VINTAGE": "Sats mined in the first 1000 bitcoin blocks.", + "PIZZA": "Sats involved in the famous pizza transaction from 2010.", + "JPEG": "Sats involved in the possible first bitcoin trade for an image on February 24, 2010.", + "HITMAN": "Sats involved in the transaction made by Ross Ulbricht to hire a hitman.", + "SILK_ROAD": "Sats seized from Silk Road and auctioned off on June 27, 2014 by US Marshals." }, "SAT_TYPES": { "RARE_SAT": "{{type}} Sat", @@ -1125,7 +1175,8 @@ "RARE_SATS_BUNDLE": "Rare Sats Bundle", "BUNDLE_PENDING_SEND_DESCRIPTION": "This bundle is already in a pending transfer.", "RARE_SAT": "Rare Sat", - "INSCRIBED_SAT": "Inscribed Sat" + "INSCRIBED_SAT": "Inscribed Sat", + "BUNDLE_SIZE": "Bundle size" }, "COLLECTIBLE_COLLECTION_SCREEN": { "BACK_TO_GALLERY": "Back to gallery",