Skip to content

Commit

Permalink
feat: add sat bundle layout to sign psbt screen (#65)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
fedeerbes committed Nov 27, 2023
1 parent 568430a commit 5499535
Show file tree
Hide file tree
Showing 8 changed files with 154 additions and 365 deletions.
108 changes: 108 additions & 0 deletions src/app/components/confirmBtcTransactionComponent/bundle.tsx
Original file line number Diff line number Diff line change
@@ -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<BundleItemContainerProps>`
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<Inscription | undefined>(undefined);

const { t } = useTranslation('translation');

return (
<>
{inscriptionToShow && (
<AssetModal
show={!!inscriptionToShow}
onClose={() => setInscriptionToShow(undefined)}
inscription={inscriptionToShow}
/>
)}
<SatsBundleContainer>
{title && <Title typography="body_medium_m">{title}</Title>}
<SatsBundleButton
type="button"
onClick={() => setShowBundleDetail((prevState) => !prevState)}
>
<Row>
<img src={BundleIcon} alt="bundle" />
<BundleTitle typography="body_medium_m" color="white_200">
{t('RARE_SATS.SATS_BUNDLE')}
</BundleTitle>
</Row>
<Row>
<BundleValue typography="body_medium_m" color="white_0">{`${bundle.totalExoticSats} ${t(
'NFT_DASHBOARD_SCREEN.RARE_SATS',
)}`}</BundleValue>
<CaretDown color={Theme.colors.white_0} size={16} />
</Row>
</SatsBundleButton>

{showBundleDetail &&
bundle.satRanges.map((item: BundleSatRange, index: number) => (
<BundleItemsContainer key={`${item.block}-${item.offset}`} addMargin={index === 0}>
<BundleItem
item={item}
ordinalEyePressed={(inscription: Inscription) => {
// show ordinal modal to show asset
setInscriptionToShow(inscription);
}}
showDivider={index !== bundle.satRanges.length - 1}
/>
</BundleItemsContainer>
))}
</SatsBundleContainer>
</>
);
}

export default SatsBundle;
94 changes: 3 additions & 91 deletions src/app/components/confirmBtcTransactionComponent/index.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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,
Expand All @@ -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;
Expand All @@ -63,10 +58,6 @@ interface ButtonProps {
isBtcSendBrowserTx?: boolean;
}

interface BundleItemContainerProps {
addMargin: boolean;
}

const ButtonContainer = styled.div<ButtonProps>((props) => ({
display: 'flex',
flexDirection: 'row',
Expand Down Expand Up @@ -115,10 +106,6 @@ const ErrorText = styled.h1((props) => ({
color: props.theme.colors.danger_medium,
}));

const BundleItemsContainer = styled.div<BundleItemContainerProps>`
margin-top: ${(props) => (props.addMargin ? Theme.space.m : 0)};
`;

interface ReviewTransactionTitleProps {
centerAligned: boolean;
}
Expand All @@ -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
Expand Down Expand Up @@ -223,8 +179,6 @@ function ConfirmBtcTransactionComponent({
const [signedTx, setSignedTx] = useState(signedTxHex);
const [total, setTotal] = useState<BigNumber>(new BigNumber(0));
const [showFeeWarning, setShowFeeWarning] = useState(false);
const [showBundleDetail, setShowBundleDetail] = useState(false);
const [inscriptionToShow, setInscriptionToShow] = useState<Inscription | undefined>(undefined);

const bundle = selectedSatBundle ?? ordinalBundle ?? undefined;
const {
Expand Down Expand Up @@ -431,13 +385,6 @@ function ConfirmBtcTransactionComponent({
<>
<OuterContainer>
{!isBtcSendBrowserTx && !isGalleryOpen && <TopRow title="" onClick={onBackButtonClick} />}
{inscriptionToShow && (
<AssetModal
show={!!inscriptionToShow}
onClose={() => setInscriptionToShow(undefined)}
inscription={inscriptionToShow}
/>
)}
<Container>
{showFeeWarning && (
<InfoContainer
Expand All @@ -460,42 +407,7 @@ function ConfirmBtcTransactionComponent({
</CalloutContainer>
)}

{bundle && (
<SatsBundleContainer>
<SatsBundle
type="button"
onClick={() => setShowBundleDetail((prevState) => !prevState)}
>
<Row>
<img src={BundleIcon} alt="bundle" />
<BundleTitle typography="body_medium_m" color="white_200">
{t('RARE_SATS.SATS_BUNDLE')}
</BundleTitle>
</Row>
<Row>
<BundleValue typography="body_medium_m" color="white_0">{`${
bundle.satributes.length
} ${t('NFT_DASHBOARD_SCREEN.RARE_SATS')}`}</BundleValue>
<CaretDown color={Theme.colors.white_0} size={16} />
</Row>
</SatsBundle>

{showBundleDetail &&
bundle.satRanges.map((item: BundleSatRange, index: number) => (
<BundleItemsContainer addMargin={index === 0}>
<BundleItem
key={`${item.block}-${item.offset}`}
item={item}
ordinalEyePressed={(inscription: Inscription) => {
// show ordinal modal to show asset
setInscriptionToShow(inscription);
}}
showDivider={index !== bundle.satRanges.length - 1}
/>
</BundleItemsContainer>
))}
</SatsBundleContainer>
)}
{bundle && <SatsBundle bundle={bundle} />}

{ordinalTxUtxo ? (
<RecipientComponent
Expand Down
2 changes: 1 addition & 1 deletion src/app/components/recipientComponent/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,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({
Expand Down
35 changes: 20 additions & 15 deletions src/app/hooks/useDetectOrdinalInSignPsbt.ts
Original file line number Diff line number Diff line change
@@ -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<BundleV2, 'value' | 'satRanges' | 'totalExoticSats'>;

const useDetectOrdinalInSignPsbt = (parsedPsbt: undefined | ParsedPSBT) => {
const [loading, setLoading] = useState(false);
const [userReceivesOrdinal, setUserReceivesOrdinal] = useState(false);
const [bundleItemsData, setBundleItemsData] = useState<BundleItem[]>([]);
const [bundleItemsData, setBundleItemsData] = useState<InputsBundle>({
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) {
Expand All @@ -37,7 +42,7 @@ const useDetectOrdinalInSignPsbt = (parsedPsbt: undefined | ParsedPSBT) => {
}),
);

setBundleItemsData(bundleItems);
setBundleItemsData({ value, satRanges, totalExoticSats });
setLoading(false);

parsedPsbt.outputs.forEach(async (output) => {
Expand Down
29 changes: 1 addition & 28 deletions src/app/screens/rareSatsBundle/rareSatsBundleGridItem.tsx
Original file line number Diff line number Diff line change
@@ -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`
Expand All @@ -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 (
<Container>
<ExoticSatsRow
Expand Down
Loading

0 comments on commit 5499535

Please sign in to comment.