From be93e72f1a6faa258aa85d30dfc70a2e94c86296 Mon Sep 17 00:00:00 2001 From: alter-eggo Date: Tue, 20 Jun 2023 11:47:41 +0400 Subject: [PATCH] feat: add brc-20 send for non-zero-index taproot addresses, closes #3830 --- src/app/components/brc20-tokens-loader.tsx | 13 ++- .../ordinals/brc20/brc20-tokens.query.ts | 97 ++++++++++++++++--- src/app/query/query-prefixes.ts | 1 + 3 files changed, 95 insertions(+), 16 deletions(-) diff --git a/src/app/components/brc20-tokens-loader.tsx b/src/app/components/brc20-tokens-loader.tsx index a01b03ad..a26d8154 100644 --- a/src/app/components/brc20-tokens-loader.tsx +++ b/src/app/components/brc20-tokens-loader.tsx @@ -1,15 +1,18 @@ import { Brc20Token, - useBrc20TokensByAddressQuery, + useBrc20TokensQuery, } from '@app/query/bitcoin/ordinals/brc20/brc20-tokens.query'; -import { useCurrentAccountTaprootIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks'; interface Brc20TokensLoaderProps { children(brc20Tokens: Brc20Token[]): JSX.Element; } export function Brc20TokensLoader({ children }: Brc20TokensLoaderProps) { - const { address: bitcoinAddressTaproot } = useCurrentAccountTaprootIndexZeroSigner(); - const { data: brc20Tokens } = useBrc20TokensByAddressQuery(bitcoinAddressTaproot); - if (!bitcoinAddressTaproot || !brc20Tokens) return null; + const { data: allBrc20TokensResponse } = useBrc20TokensQuery(); + const brc20Tokens = allBrc20TokensResponse?.pages + .flatMap(page => page.brc20Tokens) + .filter(token => token.length > 0) + .flatMap(token => token); + + if (!brc20Tokens) return null; return children(brc20Tokens); } diff --git a/src/app/query/bitcoin/ordinals/brc20/brc20-tokens.query.ts b/src/app/query/bitcoin/ordinals/brc20/brc20-tokens.query.ts index 6023f14b..742b583a 100644 --- a/src/app/query/bitcoin/ordinals/brc20/brc20-tokens.query.ts +++ b/src/app/query/bitcoin/ordinals/brc20/brc20-tokens.query.ts @@ -1,7 +1,16 @@ -import { useQuery } from '@tanstack/react-query'; +import { useCallback, useEffect } from 'react'; -import { AppUseQueryConfig } from '@app/query/query-config'; +import { useInfiniteQuery } from '@tanstack/react-query'; + +import { useAnalytics } from '@app/common/hooks/analytics/use-analytics'; +import { createNumArrayOfRange } from '@app/common/utils'; import { QueryPrefixes } from '@app/query/query-prefixes'; +import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; +import { useCurrentAccountTaprootSigner } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks'; +import { useCurrentNetwork } from '@app/store/networks/networks.selectors'; + +const addressesSimultaneousFetchLimit = 5; +const stopSearchAfterNumberAddressesWithoutBrc20Tokens = 5; interface Brc20TokenResponse { available_balance: string; @@ -53,15 +62,81 @@ async function fetchBrc20TokensByAddress(address: string): Promise }); } -type FetchBrc20TokensByAddressResp = Awaited>; +export function useBrc20TokensQuery() { + const network = useCurrentNetwork(); + const nativeSegwitSigner = useCurrentAccountNativeSegwitIndexZeroSigner(); + const currentBitcoinAddress = nativeSegwitSigner.address; + const createSigner = useCurrentAccountTaprootSigner(); + const analytics = useAnalytics(); -export function useBrc20TokensByAddressQuery( - address: string, - options?: AppUseQueryConfig -) { - return useQuery({ - queryKey: [QueryPrefixes.Brc20TokenBalance, address], - queryFn: () => fetchBrc20TokensByAddress(address), - ...options, + if (!createSigner) throw new Error('No signer'); + + const getNextTaprootAddressBatch = useCallback( + (fromIndex: number, toIndex: number) => { + return createNumArrayOfRange(fromIndex, toIndex - 1).map(num => { + const address = createSigner(num).address; + return address; + }); + }, + [createSigner] + ); + const query = useInfiniteQuery({ + queryKey: [QueryPrefixes.Brc20InfiniteQuery, currentBitcoinAddress, network.id], + async queryFn({ pageParam }) { + const fromIndex: number = pageParam?.fromIndex ?? 0; + let addressesWithoutTokens = pageParam?.addressesWithoutTokens ?? 0; + + const addressesData = getNextTaprootAddressBatch( + fromIndex, + fromIndex + addressesSimultaneousFetchLimit + ); + const brc20TokensPromises = addressesData.map(address => { + return fetchBrc20TokensByAddress(address); + }); + + const brc20Tokens = await Promise.all(brc20TokensPromises); + addressesWithoutTokens += brc20Tokens.filter(tokens => tokens.length === 0).length; + + return { + addressesWithoutTokens, + brc20Tokens, + fromIndex, + }; + }, + getNextPageParam(prevInscriptionQuery) { + const { fromIndex, brc20Tokens, addressesWithoutTokens } = prevInscriptionQuery; + + if (addressesWithoutTokens >= stopSearchAfterNumberAddressesWithoutBrc20Tokens) { + return undefined; + } + + return { + fromIndex: fromIndex + addressesSimultaneousFetchLimit, + addressesWithoutTokens, + brc20Tokens, + }; + }, + refetchOnMount: false, + refetchOnReconnect: false, + refetchOnWindowFocus: false, + staleTime: 3 * 60 * 1000, }); + + // Auto-trigger next request + useEffect(() => { + void query.fetchNextPage(); + }, [query, query.data]); + useEffect(() => { + const brc20AcrossAddressesCount = query.data?.pages.reduce((acc, page) => { + return acc + page.brc20Tokens.flatMap(item => item).length; + }, 0); + + if (!query.hasNextPage && brc20AcrossAddressesCount && brc20AcrossAddressesCount > 0) { + void analytics.identify({ + brc20_across_addresses_count: brc20AcrossAddressesCount, + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [analytics, query.hasNextPage]); + return query; } diff --git a/src/app/query/query-prefixes.ts b/src/app/query/query-prefixes.ts index 8ac2d3a9..bf931975 100644 --- a/src/app/query/query-prefixes.ts +++ b/src/app/query/query-prefixes.ts @@ -16,4 +16,5 @@ export enum QueryPrefixes { StampCollection = 'stamp-collection', StampsByAddress = 'stamps-by-address', + Brc20InfiniteQuery = 'brc20-infinite-query', }