refactor: stamps query and ui, closes #3648

This commit is contained in:
fbwoolf
2023-05-03 12:35:51 -05:00
committed by Fara Woolf
parent 85e5f9a6b6
commit 501ac5f715
16 changed files with 119 additions and 118 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -0,0 +1,38 @@
import { FiCopy } from 'react-icons/fi';
import { Box, Button, Flex, Stack } from '@stacks/ui';
import { truncateMiddle } from '@stacks/ui-utils';
import { Flag } from '../layout/flag';
import { Caption } from '../typography';
interface ReceiveCollectibleItemProps {
address: string;
icon: JSX.Element;
onCopyAddress(): void;
title: string;
}
export function ReceiveCollectibleItem({
address,
icon,
onCopyAddress,
title,
}: ReceiveCollectibleItemProps) {
return (
<Flag img={icon} spacing="base">
<Flex justifyContent="space-between">
<Box>
{title}
<Caption mt="2px">{truncateMiddle(address, 6)}</Caption>
</Box>
<Stack>
<Box>
<Button borderRadius="10px" mode="tertiary" onClick={onCopyAddress}>
<FiCopy />
</Button>
</Box>
</Stack>
</Flex>
</Flag>
);
}

View File

@@ -1,9 +1,8 @@
import toast from 'react-hot-toast';
import { FiCopy } from 'react-icons/fi';
import { useLocation, useNavigate } from 'react-router-dom';
import { Box, Button, Flex, Stack, useClipboard } from '@stacks/ui';
import { truncateMiddle } from '@stacks/ui-utils';
import BitcoinStampImg from '@assets/images/bitcoin-stamp.png';
import { Box, Stack, useClipboard } from '@stacks/ui';
import get from 'lodash.get';
import { RouteUrls } from '@shared/route-urls';
@@ -11,77 +10,68 @@ import { RouteUrls } from '@shared/route-urls';
import { useAnalytics } from '@app/common/hooks/analytics/use-analytics';
import { StxAvatar } from '@app/components/crypto-assets/stacks/components/stx-avatar';
import { OrdinalIcon } from '@app/components/icons/ordinal-icon';
import { Flag } from '@app/components/layout/flag';
import { Caption } from '@app/components/typography';
import { useZeroIndexTaprootAddress } from '@app/query/bitcoin/ordinals/use-zero-index-taproot-address';
import { useCurrentBtcNativeSegwitAccountAddressIndexZero } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import { useCurrentAccountStxAddressState } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks';
import { ReceiveCollectibleItem } from './receive-collectible-item';
export function ReceiveCollectible() {
const analytics = useAnalytics();
const location = useLocation();
const navigate = useNavigate();
const accountIndex = get(location.state, 'accountIndex', undefined);
const btcAddress = useZeroIndexTaprootAddress(accountIndex);
const btcAddressNativeSegwit = useCurrentBtcNativeSegwitAccountAddressIndexZero();
const btcAddressTaproot = useZeroIndexTaprootAddress(accountIndex);
// TODO: Reuse later for privacy mode
// const { isLoading, isError, data: btcAddress } = useNextFreshTaprootAddressQuery(accountIndex);
const stxAddress = useCurrentAccountStxAddressState();
const { onCopy: onCopyBitcoin } = useClipboard(btcAddressNativeSegwit);
const { onCopy: onCopyStacks } = useClipboard(stxAddress);
function copyToClipboard(copyHandler: () => void) {
function copyBitcoinAddressToClipboard(copyHandler: () => void) {
void analytics.track('select_stamp_to_add_new_collectible');
toast.success('Copied to clipboard!');
copyHandler();
}
function copyStacksAddressToClipboard(copyHandler: () => void) {
void analytics.track('select_nft_to_add_new_collectible');
toast.success('Copied to clipboard!');
copyHandler();
}
if (!btcAddress) return null;
if (!btcAddressTaproot) return null;
return (
<Stack spacing="loose" mt="base" mb="extra-loose">
<Flag img={<OrdinalIcon />} spacing="base">
<Flex justifyContent="space-between">
<ReceiveCollectibleItem
address={btcAddressTaproot}
icon={<OrdinalIcon />}
onCopyAddress={() => {
void analytics.track('select_inscription_to_add_new_collectible');
navigate(RouteUrls.ReceiveCollectibleOrdinal, { state: { btcAddressTaproot } });
}}
title="Ordinal inscription"
/>
<ReceiveCollectibleItem
address={btcAddressNativeSegwit}
icon={
<Box>
Ordinal inscription
<Caption mt="2px">{truncateMiddle(btcAddress, 6)}</Caption>
<img src={BitcoinStampImg} width="36px" />
</Box>
<Stack>
<Box>
<Button
borderRadius="10px"
mode="tertiary"
onClick={() => {
void analytics.track('select_inscription_to_add_new_collectible');
navigate(RouteUrls.ReceiveCollectibleOrdinal, { state: { btcAddress } });
}}
>
<FiCopy />
</Button>
</Box>
</Stack>
</Flex>
</Flag>
<Flag img={<StxAvatar />} spacing="base">
<Flex justifyContent="space-between">
<Box>
Stacks NFT
<Caption mt="2px">{truncateMiddle(stxAddress, 6)}</Caption>
</Box>
<Stack>
<Box>
<Button
borderRadius="10px"
mode="tertiary"
onClick={() => {
copyToClipboard(onCopyStacks);
}}
>
<FiCopy />
</Button>
</Box>
</Stack>
</Flex>
</Flag>
}
onCopyAddress={() => copyBitcoinAddressToClipboard(onCopyBitcoin)}
title="Bitcoin Stamp"
/>
<ReceiveCollectibleItem
address={stxAddress}
icon={<StxAvatar />}
onCopyAddress={() => copyStacksAddressToClipboard(onCopyStacks)}
title="Stacks NFT"
/>
</Stack>
);
}

View File

@@ -3,30 +3,30 @@ import { useState } from 'react';
import { Spinner } from '@stacks/ui';
import { figmaTheme } from '@app/common/utils/figma-theme';
import { OrdinalMinimalIcon } from '@app/components/icons/ordinal-minimal-icon';
import { CollectibleItemLayout, CollectibleItemLayoutProps } from '../collectible-item.layout';
import { ImageUnavailable } from '../image-unavailable';
interface CollectibleImageProps extends Omit<CollectibleItemLayoutProps, 'children'> {
alt?: string;
icon: JSX.Element;
src: string;
}
export function CollectibleImage(props: CollectibleImageProps) {
const { alt, src, ...rest } = props;
const { alt, icon, src, ...rest } = props;
const [isError, setIsError] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [width, setWidth] = useState(0);
if (isError)
return (
<CollectibleItemLayout collectibleTypeIcon={<OrdinalMinimalIcon />} {...rest}>
<CollectibleItemLayout collectibleTypeIcon={icon} {...rest}>
<ImageUnavailable />
</CollectibleItemLayout>
);
return (
<CollectibleItemLayout collectibleTypeIcon={<OrdinalMinimalIcon />} {...rest}>
<CollectibleItemLayout collectibleTypeIcon={icon} {...rest}>
{isLoading && <Spinner color={figmaTheme.icon} size="16px" />}
<img
alt={alt}

View File

@@ -1,15 +1,14 @@
import { OrdinalMinimalIcon } from '@app/components/icons/ordinal-minimal-icon';
import { CollectibleItemLayout, CollectibleItemLayoutProps } from '../collectible-item.layout';
import { CollectibleTextLayout } from './collectible-text.layout';
interface CollectibleTextProps extends Omit<CollectibleItemLayoutProps, 'children'> {
icon: JSX.Element;
content: string;
}
export function CollectibleText(props: CollectibleTextProps) {
const { content, ...rest } = props;
const { content, icon, ...rest } = props;
return (
<CollectibleItemLayout collectibleTypeIcon={<OrdinalMinimalIcon />} {...rest}>
<CollectibleItemLayout collectibleTypeIcon={icon} {...rest}>
<CollectibleTextLayout>{content}</CollectibleTextLayout>
</CollectibleItemLayout>
);

View File

@@ -33,7 +33,7 @@ export function AddCollectible() {
void analytics.track('select_add_new_collectible');
navigate(RouteUrls.ReceiveCollectible);
}}
subtitle="Ordinal or Stacks NFT"
subtitle="Collectible"
title="Add new"
>
<Box as={Plus} width="40px" />

View File

@@ -1,6 +1,7 @@
import { Spinner } from '@stacks/ui';
import { figmaTheme } from '@app/common/utils/figma-theme';
import { OrdinalMinimalIcon } from '@app/components/icons/ordinal-minimal-icon';
import { useTextInscriptionContentQuery } from '@app/query/bitcoin/ordinals/use-text-ordinal-content.query';
import { CollectibleText } from '../_collectible-types/collectible-text';
@@ -24,6 +25,7 @@ export function InscriptionText({
return (
<CollectibleText
icon={<OrdinalMinimalIcon />}
key={inscriptionNumber}
onClickCallToAction={onClickCallToAction}
onClickSend={onClickSend}

View File

@@ -4,6 +4,7 @@ import { RouteUrls } from '@shared/route-urls';
import { openInNewTab } from '@app/common/utils/open-in-new-tab';
import { OrdinalIconFull } from '@app/components/icons/ordinal-icon-full';
import { OrdinalMinimalIcon } from '@app/components/icons/ordinal-minimal-icon';
import { useInscription } from '@app/query/bitcoin/ordinals/inscription.hooks';
import { TaprootUtxo } from '@app/query/bitcoin/ordinals/use-taproot-address-utxos.query';
@@ -32,6 +33,7 @@ export function Inscription({ path, utxo }: InscriptionProps) {
case 'image': {
return (
<CollectibleImage
icon={<OrdinalMinimalIcon />}
key={inscription.title}
onClickCallToAction={() => openInNewTab(inscription.infoUrl)}
onClickSend={() => openSendInscriptionModal()}

View File

@@ -1,5 +1,8 @@
import BitcoinStampImg from '@assets/images/bitcoin-stamp.png';
import { Box } from '@stacks/ui';
import { openInNewTab } from '@app/common/utils/open-in-new-tab';
import { Stamp as BitcoinStamp } from '@app/query/bitcoin/stamps/stamp-collection.query';
import { Stamp as BitcoinStamp } from '@app/query/bitcoin/stamps/stamps-by-address.query';
import { CollectibleImage } from '../_collectible-types/collectible-image';
@@ -10,6 +13,11 @@ export function Stamp(props: { bitcoinStamp: BitcoinStamp }) {
return (
<CollectibleImage
icon={
<Box>
<img src={BitcoinStampImg} width="30px" />
</Box>
}
key={bitcoinStamp.stamp}
onClickCallToAction={() => openInNewTab(`${stampChainAssetUrl}${bitcoinStamp.stamp}`)}
src={bitcoinStamp.stamp_url}

View File

@@ -1,6 +1,7 @@
import StacksNftBns from '@assets/images/stacks-nft-bns.png';
import { figmaTheme } from '@app/common/utils/figma-theme';
import { StxAvatar } from '@app/components/crypto-assets/stacks/components/stx-avatar';
import { CollectibleItemLayout } from '../collectible-item.layout';
@@ -16,6 +17,7 @@ export function StacksBnsName(props: { bnsName: string }) {
return (
<CollectibleItemLayout
backgroundElementProps={backgroundProps}
collectibleTypeIcon={<StxAvatar size="30px" />}
subtitle="Bitcoin Naming System"
title={bnsName}
>

View File

@@ -25,7 +25,7 @@ export function StacksNonFungibleTokens({ metadata }: StacksNonFungibleTokensPro
<CollectibleImage
alt="stacks nft"
backgroundElementProps={backgroundProps}
collectibleTypeIcon={<StxAvatar size="30px" />}
icon={<StxAvatar size="30px" />}
src={metadata.cached_image ?? ''}
subtitle="Stacks NFT"
title={metadata.name ?? ''}

View File

@@ -7,7 +7,6 @@ import { useBitcoinClient } from '@app/store/common/api-clients.hooks';
import { useCurrentNetwork } from '@app/store/networks/networks.selectors';
import { UtxoResponseItem } from '../bitcoin-client';
import { useIsStampedTx } from '../stamps/use-is-stamped-tx';
import { getTaprootAddress, hasInscriptions } from './utils';
const stopSearchAfterNumberAddressesWithoutOrdinals = 20;
@@ -24,7 +23,6 @@ export interface TaprootUtxo extends UtxoResponseItem {
export function useTaprootAccountUtxosQuery() {
const network = useCurrentNetwork();
const keychain = useCurrentTaprootAccountKeychain();
const isStamped = useIsStampedTx();
const client = useBitcoinClient();
const currentAccountIndex = useCurrentAccountIndex();
@@ -62,6 +60,6 @@ export function useTaprootAccountUtxosQuery() {
currentNumberOfAddressesWithoutOrdinals = 0;
addressIndexCounter.increment();
}
return foundUnspentTransactions.filter(tx => !isStamped(tx.txid));
return foundUnspentTransactions;
});
}

View File

@@ -1,42 +0,0 @@
import { useQuery } from '@tanstack/react-query';
import { AppUseQueryConfig } from '@app/query/query-config';
import { QueryPrefixes } from '@app/query/query-prefixes';
export interface Stamp {
asset: string;
block_index: number;
message_index: number;
stamp: number;
stamp_mimetype: string;
stamp_url: string;
timestamp: number;
tx_hash: string;
tx_index: number;
}
async function fetchStampCollection() {
return (
fetch('https://stampchain.io/stamp.json')
.then(res => res.json())
// Currently we only filter UTXOs for stamps. As we collect, and cache,
// all stamps, we cache only the ID to lower the amount of storage used
.then((allStamps: Stamp[]) => allStamps.map(s => ({ tx_hash: s.tx_hash })))
);
}
type FetchStampCollectionResp = Awaited<ReturnType<typeof fetchStampCollection>>;
export function useStampCollectionQuery<T extends unknown = FetchStampCollectionResp>(
options?: AppUseQueryConfig<FetchStampCollectionResp, T>
) {
return useQuery({
queryKey: [QueryPrefixes.StampCollection],
queryFn: () => fetchStampCollection(),
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
staleTime: 1000 * 60 * 60 * 5,
...options,
});
}

View File

@@ -3,12 +3,17 @@ import { useQuery } from '@tanstack/react-query';
import { AppUseQueryConfig } from '@app/query/query-config';
import { QueryPrefixes } from '@app/query/query-prefixes';
import { Stamp } from './stamp-collection.query';
const stampsByAddressQueryOptions = {
cacheTime: Infinity,
staleTime: 15 * 60 * 1000,
} as const;
export interface Stamp {
asset: string;
block_index: number;
message_index: number;
stamp: number;
stamp_mimetype: string;
stamp_url: string;
timestamp: number;
tx_hash: string;
tx_index: number;
}
async function fetchStampsByAddress(address: string): Promise<Stamp[]> {
return fetch(`https://stampchain.io/api/stamps?wallet_address=${address}`).then(res =>
@@ -25,7 +30,6 @@ export function useStampsByAddressQuery<T extends unknown = FetchStampsByAddress
return useQuery({
queryKey: [QueryPrefixes.StampsByAddress],
queryFn: () => fetchStampsByAddress(address),
...stampsByAddressQueryOptions,
...options,
});
}

View File

@@ -1,12 +1,12 @@
import { useCallback } from 'react';
import { useStampCollectionQuery } from './stamp-collection.query';
import { useCurrentBtcNativeSegwitAccountAddressIndexZero } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import { useStampsByAddressQuery } from './stamps-by-address.query';
export function useIsStampedTx() {
const { data: stampCollection = [] } = useStampCollectionQuery();
const currentAccountBtcAddress = useCurrentBtcNativeSegwitAccountAddressIndexZero();
const { data: stamps = [] } = useStampsByAddressQuery(currentAccountBtcAddress);
return useCallback(
(txid: string) => stampCollection.some(stamp => stamp.tx_hash === txid),
[stampCollection]
);
return useCallback((txid: string) => stamps.some(stamp => stamp.tx_hash === txid), [stamps]);
}