From 54995359fbbf2b7a05e6a3c34608e1ddce01faaa Mon Sep 17 00:00:00 2001 From: fede erbes Date: Mon, 27 Nov 2023 09:19:25 +0100 Subject: [PATCH] feat: add sat bundle layout to sign psbt screen (#65) * chore: move bundle from ConfirmBtcTransactionComponent to it own component * chore: use getSatLabel from utils in RareSatsBundleGridItem * feat: tweak useDetectOrdinalInSignPsbt hook to use V2 of rare sats api and implement sats bundle component in SignPsbtRequest screen --- .../confirmBtcTransactionComponent/bundle.tsx | 108 +++++++++ .../confirmBtcTransactionComponent/index.tsx | 94 +------- .../components/recipientComponent/index.tsx | 2 +- src/app/hooks/useDetectOrdinalInSignPsbt.ts | 35 +-- .../rareSatsBundle/rareSatsBundleGridItem.tsx | 29 +-- .../signPsbtRequest/bundleItemsComponent.tsx | 219 ------------------ src/app/screens/signPsbtRequest/index.tsx | 29 ++- src/locales/en.json | 3 +- 8 files changed, 154 insertions(+), 365 deletions(-) create mode 100644 src/app/components/confirmBtcTransactionComponent/bundle.tsx delete mode 100644 src/app/screens/signPsbtRequest/bundleItemsComponent.tsx diff --git a/src/app/components/confirmBtcTransactionComponent/bundle.tsx b/src/app/components/confirmBtcTransactionComponent/bundle.tsx new file mode 100644 index 000000000..f0f864c3d --- /dev/null +++ b/src/app/components/confirmBtcTransactionComponent/bundle.tsx @@ -0,0 +1,108 @@ +import BundleIcon from '@assets/img/rareSats/satBundle.svg'; +import AssetModal from '@components/assetModal'; +import { CaretDown } from '@phosphor-icons/react'; +import { StyledP } from '@ui-library/common.styled'; +import { BundleSatRange, BundleV2, Inscription } from '@utils/rareSats'; +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: BundleV2; 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/index.tsx b/src/app/components/confirmBtcTransactionComponent/index.tsx index 5e07116e7..84e40e7ae 100644 --- a/src/app/components/confirmBtcTransactionComponent/index.tsx +++ b/src/app/components/confirmBtcTransactionComponent/index.tsx @@ -1,7 +1,5 @@ import SettingIcon from '@assets/img/dashboard/faders_horizontal.svg'; -import BundleIcon from '@assets/img/rareSats/satBundle.svg'; import AssetIcon from '@assets/img/transactions/Assets.svg'; -import AssetModal from '@components/assetModal'; import ActionButton from '@components/button'; import InfoContainer from '@components/infoContainer'; import RecipientComponent from '@components/recipientComponent'; @@ -12,7 +10,6 @@ import useNftDataSelector from '@hooks/stores/useNftDataSelector'; import useOrdinalsByAddress from '@hooks/useOrdinalsByAddress'; import useSeedVault from '@hooks/useSeedVault'; import useWalletSelector from '@hooks/useWalletSelector'; -import { CaretDown } from '@phosphor-icons/react'; import { ErrorCodes, getBtcFiatEquivalent, @@ -29,17 +26,15 @@ import { } from '@secretkeylabs/xverse-core/transactions/btc'; import { useMutation } from '@tanstack/react-query'; import Callout from '@ui-library/callout'; -import { StyledP } from '@ui-library/common.styled'; import { CurrencyTypes } from '@utils/constants'; -import { BundleSatRange, BundleV2, Inscription } from '@utils/rareSats'; +import { BundleV2 } from '@utils/rareSats'; import BigNumber from 'bignumber.js'; import { ReactNode, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { NumericFormat } from 'react-number-format'; import styled from 'styled-components'; -import Theme from 'theme'; import TransactionDetailComponent from '../transactionDetailComponent'; -import { BundleItem } from './bundleItem'; +import SatsBundle from './bundle'; const OuterContainer = styled.div` display: flex; @@ -63,10 +58,6 @@ interface ButtonProps { isBtcSendBrowserTx?: boolean; } -interface BundleItemContainerProps { - addMargin: boolean; -} - const ButtonContainer = styled.div((props) => ({ display: 'flex', flexDirection: 'row', @@ -115,10 +106,6 @@ const ErrorText = styled.h1((props) => ({ color: props.theme.colors.danger_medium, })); -const BundleItemsContainer = styled.div` - margin-top: ${(props) => (props.addMargin ? Theme.space.m : 0)}; -`; - interface ReviewTransactionTitleProps { centerAligned: boolean; } @@ -134,37 +121,6 @@ const CalloutContainer = styled.div((props) => ({ marginhorizontal: props.theme.spacing(8), })); -const SatsBundle = styled.button` - display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; - background-color: ${(props) => props.theme.colors.elevation1}; -`; - -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 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}; -`; - interface Props { currentFee: BigNumber; feePerVByte: BigNumber; // TODO tim: is this the same as currentFeeRate? refactor to be clear @@ -223,8 +179,6 @@ function ConfirmBtcTransactionComponent({ const [signedTx, setSignedTx] = useState(signedTxHex); const [total, setTotal] = useState(new BigNumber(0)); const [showFeeWarning, setShowFeeWarning] = useState(false); - const [showBundleDetail, setShowBundleDetail] = useState(false); - const [inscriptionToShow, setInscriptionToShow] = useState(undefined); const bundle = selectedSatBundle ?? ordinalBundle ?? undefined; const { @@ -431,13 +385,6 @@ function ConfirmBtcTransactionComponent({ <> {!isBtcSendBrowserTx && !isGalleryOpen && } - {inscriptionToShow && ( - setInscriptionToShow(undefined)} - inscription={inscriptionToShow} - /> - )} {showFeeWarning && ( )} - {bundle && ( - - setShowBundleDetail((prevState) => !prevState)} - > - - bundle - - {t('RARE_SATS.SATS_BUNDLE')} - - - - {`${ - bundle.satributes.length - } ${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} - /> - - ))} - - )} + {bundle && } {ordinalTxUtxo ? ( ({ 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({ diff --git a/src/app/hooks/useDetectOrdinalInSignPsbt.ts b/src/app/hooks/useDetectOrdinalInSignPsbt.ts index 36fe692b7..fa0a59a8e 100644 --- a/src/app/hooks/useDetectOrdinalInSignPsbt.ts +++ b/src/app/hooks/useDetectOrdinalInSignPsbt.ts @@ -1,32 +1,37 @@ -import { getUtxoOrdinalBundle, ParsedPSBT } from '@secretkeylabs/xverse-core'; -import { BundleItem, mapRareSatsAPIResponseToRareSats } from '@utils/rareSats'; +import { ParsedPSBT } from '@secretkeylabs/xverse-core'; +import { BundleSatRange, BundleV2, mapRareSatsAPIResponseToRareSatsV2 } from '@utils/rareSats'; import { isAxiosError } from 'axios'; import { useEffect, useState } from 'react'; +import { getUtxoOrdinalBundleV2 } from './queries/ordinals/useAddressRareSats'; import useWalletSelector from './useWalletSelector'; +type InputsBundle = Pick; + const useDetectOrdinalInSignPsbt = (parsedPsbt: undefined | ParsedPSBT) => { const [loading, setLoading] = useState(false); const [userReceivesOrdinal, setUserReceivesOrdinal] = useState(false); - const [bundleItemsData, setBundleItemsData] = useState([]); + const [bundleItemsData, setBundleItemsData] = useState({ + value: 0, + satRanges: [], + totalExoticSats: 0, + }); const { ordinalsAddress, network } = useWalletSelector(); async function handleOrdinalAndOrdinalInfo() { - const bundleItems: BundleItem[] = []; + const satRanges: BundleSatRange[] = []; + let value = 0; + let totalExoticSats = 0; if (parsedPsbt) { setLoading(true); await Promise.all( 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); - }); + const data = await getUtxoOrdinalBundleV2(network.type, input.txid, input.index); + + const bundle = mapRareSatsAPIResponseToRareSatsV2(data); + satRanges.push(...bundle.satRanges); + value += bundle.value; + totalExoticSats += bundle.totalExoticSats; } 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) { @@ -37,7 +42,7 @@ const useDetectOrdinalInSignPsbt = (parsedPsbt: undefined | ParsedPSBT) => { }), ); - setBundleItemsData(bundleItems); + setBundleItemsData({ value, satRanges, totalExoticSats }); setLoading(false); parsedPsbt.outputs.forEach(async (output) => { diff --git a/src/app/screens/rareSatsBundle/rareSatsBundleGridItem.tsx b/src/app/screens/rareSatsBundle/rareSatsBundleGridItem.tsx index 91ea7c8a9..e4fcc4f37 100644 --- a/src/app/screens/rareSatsBundle/rareSatsBundleGridItem.tsx +++ b/src/app/screens/rareSatsBundle/rareSatsBundleGridItem.tsx @@ -1,13 +1,6 @@ import ExoticSatsRow from '@components/exoticSatsRow/exoticSatsRow'; import RareSatIcon from '@components/rareSatIcon/rareSatIcon'; -import { - BundleSatRange, - getRareSatsLabelByType, - RareSatsType, - RoadArmorRareSats, - RoadArmorRareSatsType, -} from '@utils/rareSats'; -import { useTranslation } from 'react-i18next'; +import { BundleSatRange, getSatLabel } from '@utils/rareSats'; import styled from 'styled-components'; const RangeContainer = styled.div` @@ -30,26 +23,6 @@ const Container = styled.div((props) => ({ })); export function RareSatsBundleGridItem({ item }: { item: BundleSatRange }) { - const { t } = useTranslation('translation'); - - const getSatLabel = (satributes: RareSatsType[]) => { - const isLengthGrateThanTwo = satributes.length > 2; - if (satributes.length === 1) { - return getRareSatsLabelByType(satributes[0]); - } - - // we expect to roadarmor sats be in the first position - if (RoadArmorRareSats.includes(satributes[0] as RoadArmorRareSatsType)) { - return `${getRareSatsLabelByType(satributes[0])} ${t( - isLengthGrateThanTwo ? 'COMMON.COMBO' : `RARE_SATS.RARITY_LABEL.${satributes[1]}`, - )}`; - } - - return isLengthGrateThanTwo - ? t('COMMON.COMBO') - : `${getRareSatsLabelByType(satributes[0])} ${getRareSatsLabelByType(satributes[1])}`; - }; - return ( ({ - 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 f85e8297f..201ba64c9 100644 --- a/src/app/screens/signPsbtRequest/index.tsx +++ b/src/app/screens/signPsbtRequest/index.tsx @@ -5,6 +5,7 @@ import { ledgerDelay } 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'; @@ -19,6 +20,7 @@ import { getBtcFiatEquivalent, satsToBtc, signLedgerPSBT } from '@secretkeylabs/ import { Transport as TransportType } from '@secretkeylabs/xverse-core/ledger/types'; import { parsePsbt, psbtBase64ToHex } from '@secretkeylabs/xverse-core/transactions/psbt'; import { isLedgerAccount } from '@utils/helper'; +import { BundleV2 } from '@utils/rareSats'; import BigNumber from 'bignumber.js'; import { decodeToken } from 'jsontokens'; import { useEffect, useMemo, useState } from 'react'; @@ -28,7 +30,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; @@ -105,6 +106,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); @@ -352,15 +356,12 @@ function SignPsbtRequest() { {t('REVIEW_TRANSACTION')} {!payload.broadcast && } - {bundleItemsData && - bundleItemsData.map((bundleItem, index) => ( - - ))} + {bundleItemsData && ( + + )} + {payload.broadcast ? (