mirror of
https://github.com/zhigang1992/wallet.git
synced 2026-05-13 11:58:13 +08:00
refactor: stamps query and ui, closes #3648
This commit is contained in:
BIN
public/assets/images/bitcoin-stamp.png
Normal file
BIN
public/assets/images/bitcoin-stamp.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 34 KiB |
38
src/app/components/receive/receive-collectible-item.tsx
Normal file
38
src/app/components/receive/receive-collectible-item.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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 ?? ''}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user