diff --git a/public/assets/images/bitcoin-stamp.png b/public/assets/images/bitcoin-stamp.png new file mode 100644 index 00000000..cc129286 Binary files /dev/null and b/public/assets/images/bitcoin-stamp.png differ diff --git a/public/assets/images/generic-error-icon.png b/public/assets/images/generic-error-icon.png deleted file mode 100644 index 3393601d..00000000 Binary files a/public/assets/images/generic-error-icon.png and /dev/null differ diff --git a/src/app/components/receive/receive-collectible-item.tsx b/src/app/components/receive/receive-collectible-item.tsx new file mode 100644 index 00000000..45d4b382 --- /dev/null +++ b/src/app/components/receive/receive-collectible-item.tsx @@ -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 ( + + + + {title} + {truncateMiddle(address, 6)} + + + + + + + + + ); +} diff --git a/src/app/components/receive/receive-collectible.tsx b/src/app/components/receive/receive-collectible.tsx index 9cac95ad..a9aeb291 100644 --- a/src/app/components/receive/receive-collectible.tsx +++ b/src/app/components/receive/receive-collectible.tsx @@ -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 ( - } spacing="base"> - + } + onCopyAddress={() => { + void analytics.track('select_inscription_to_add_new_collectible'); + navigate(RouteUrls.ReceiveCollectibleOrdinal, { state: { btcAddressTaproot } }); + }} + title="Ordinal inscription" + /> + - Ordinal inscription - {truncateMiddle(btcAddress, 6)} + - - - - - - - - } spacing="base"> - - - Stacks NFT - {truncateMiddle(stxAddress, 6)} - - - - - - - - + } + onCopyAddress={() => copyBitcoinAddressToClipboard(onCopyBitcoin)} + title="Bitcoin Stamp" + /> + } + onCopyAddress={() => copyStacksAddressToClipboard(onCopyStacks)} + title="Stacks NFT" + /> ); } diff --git a/src/app/features/collectibles/components/_collectible-types/collectible-image.tsx b/src/app/features/collectibles/components/_collectible-types/collectible-image.tsx index 8db6296d..25310617 100644 --- a/src/app/features/collectibles/components/_collectible-types/collectible-image.tsx +++ b/src/app/features/collectibles/components/_collectible-types/collectible-image.tsx @@ -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 { 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 ( - } {...rest}> + ); return ( - } {...rest}> + {isLoading && } {alt} { + icon: JSX.Element; content: string; } export function CollectibleText(props: CollectibleTextProps) { - const { content, ...rest } = props; + const { content, icon, ...rest } = props; return ( - } {...rest}> + {content} ); diff --git a/src/app/features/collectibles/components/add-collectible.tsx b/src/app/features/collectibles/components/add-collectible.tsx index 26d52130..e1bda496 100644 --- a/src/app/features/collectibles/components/add-collectible.tsx +++ b/src/app/features/collectibles/components/add-collectible.tsx @@ -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" > diff --git a/src/app/features/collectibles/components/bitcoin/inscription-text.tsx b/src/app/features/collectibles/components/bitcoin/inscription-text.tsx index 25714992..2b90171c 100644 --- a/src/app/features/collectibles/components/bitcoin/inscription-text.tsx +++ b/src/app/features/collectibles/components/bitcoin/inscription-text.tsx @@ -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 ( } key={inscriptionNumber} onClickCallToAction={onClickCallToAction} onClickSend={onClickSend} diff --git a/src/app/features/collectibles/components/bitcoin/inscription.tsx b/src/app/features/collectibles/components/bitcoin/inscription.tsx index 5eb76604..3f2a43cf 100644 --- a/src/app/features/collectibles/components/bitcoin/inscription.tsx +++ b/src/app/features/collectibles/components/bitcoin/inscription.tsx @@ -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 ( } key={inscription.title} onClickCallToAction={() => openInNewTab(inscription.infoUrl)} onClickSend={() => openSendInscriptionModal()} diff --git a/src/app/features/collectibles/components/bitcoin/stamp.tsx b/src/app/features/collectibles/components/bitcoin/stamp.tsx index 4891e77c..cb768a2a 100644 --- a/src/app/features/collectibles/components/bitcoin/stamp.tsx +++ b/src/app/features/collectibles/components/bitcoin/stamp.tsx @@ -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 ( + + + } key={bitcoinStamp.stamp} onClickCallToAction={() => openInNewTab(`${stampChainAssetUrl}${bitcoinStamp.stamp}`)} src={bitcoinStamp.stamp_url} diff --git a/src/app/features/collectibles/components/stacks/stacks-bns-name.tsx b/src/app/features/collectibles/components/stacks/stacks-bns-name.tsx index 112d4a68..fdf0d779 100644 --- a/src/app/features/collectibles/components/stacks/stacks-bns-name.tsx +++ b/src/app/features/collectibles/components/stacks/stacks-bns-name.tsx @@ -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 ( } subtitle="Bitcoin Naming System" title={bnsName} > diff --git a/src/app/features/collectibles/components/stacks/stacks-non-fungible-tokens.tsx b/src/app/features/collectibles/components/stacks/stacks-non-fungible-tokens.tsx index a4a2ea9a..77e3f08e 100644 --- a/src/app/features/collectibles/components/stacks/stacks-non-fungible-tokens.tsx +++ b/src/app/features/collectibles/components/stacks/stacks-non-fungible-tokens.tsx @@ -25,7 +25,7 @@ export function StacksNonFungibleTokens({ metadata }: StacksNonFungibleTokensPro } + icon={} src={metadata.cached_image ?? ''} subtitle="Stacks NFT" title={metadata.name ?? ''} diff --git a/src/app/query/bitcoin/ordinals/use-taproot-address-utxos.query.ts b/src/app/query/bitcoin/ordinals/use-taproot-address-utxos.query.ts index b8b8f156..f48cbace 100644 --- a/src/app/query/bitcoin/ordinals/use-taproot-address-utxos.query.ts +++ b/src/app/query/bitcoin/ordinals/use-taproot-address-utxos.query.ts @@ -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; }); } diff --git a/src/app/query/bitcoin/stamps/stamp-collection.query.ts b/src/app/query/bitcoin/stamps/stamp-collection.query.ts deleted file mode 100644 index c2106a52..00000000 --- a/src/app/query/bitcoin/stamps/stamp-collection.query.ts +++ /dev/null @@ -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>; - -export function useStampCollectionQuery( - options?: AppUseQueryConfig -) { - return useQuery({ - queryKey: [QueryPrefixes.StampCollection], - queryFn: () => fetchStampCollection(), - refetchOnMount: false, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - staleTime: 1000 * 60 * 60 * 5, - ...options, - }); -} diff --git a/src/app/query/bitcoin/stamps/stamps-by-address.query.ts b/src/app/query/bitcoin/stamps/stamps-by-address.query.ts index 638bd07d..e786ac88 100644 --- a/src/app/query/bitcoin/stamps/stamps-by-address.query.ts +++ b/src/app/query/bitcoin/stamps/stamps-by-address.query.ts @@ -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 { return fetch(`https://stampchain.io/api/stamps?wallet_address=${address}`).then(res => @@ -25,7 +30,6 @@ export function useStampsByAddressQuery fetchStampsByAddress(address), - ...stampsByAddressQueryOptions, ...options, }); } diff --git a/src/app/query/bitcoin/stamps/use-is-stamped-tx.ts b/src/app/query/bitcoin/stamps/use-is-stamped-tx.ts index 5457e888..76a2b089 100644 --- a/src/app/query/bitcoin/stamps/use-is-stamped-tx.ts +++ b/src/app/query/bitcoin/stamps/use-is-stamped-tx.ts @@ -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]); }