Skip to content

Commit

Permalink
feat: add brc-20 send for non-zero-index taproot addresses, closes le…
Browse files Browse the repository at this point in the history
  • Loading branch information
alter-eggo committed Jun 21, 2023
1 parent 950a28a commit be93e72
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 16 deletions.
13 changes: 8 additions & 5 deletions src/app/components/brc20-tokens-loader.tsx
Original file line number Diff line number Diff line change
@@ -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);
}
97 changes: 86 additions & 11 deletions src/app/query/bitcoin/ordinals/brc20/brc20-tokens.query.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -53,15 +62,81 @@ async function fetchBrc20TokensByAddress(address: string): Promise<Brc20Token[]>
});
}

type FetchBrc20TokensByAddressResp = Awaited<ReturnType<typeof fetchBrc20TokensByAddress>>;
export function useBrc20TokensQuery() {
const network = useCurrentNetwork();
const nativeSegwitSigner = useCurrentAccountNativeSegwitIndexZeroSigner();
const currentBitcoinAddress = nativeSegwitSigner.address;
const createSigner = useCurrentAccountTaprootSigner();
const analytics = useAnalytics();

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);
});

export function useBrc20TokensByAddressQuery<T extends unknown = FetchBrc20TokensByAddressResp>(
address: string,
options?: AppUseQueryConfig<FetchBrc20TokensByAddressResp, T>
) {
return useQuery({
queryKey: [QueryPrefixes.Brc20TokenBalance, address],
queryFn: () => fetchBrc20TokensByAddress(address),
...options,
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;
}
1 change: 1 addition & 0 deletions src/app/query/query-prefixes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ export enum QueryPrefixes {

StampCollection = 'stamp-collection',
StampsByAddress = 'stamps-by-address',
Brc20InfiniteQuery = 'brc20-infinite-query',
}

0 comments on commit be93e72

Please sign in to comment.