Skip to content

Commit

Permalink
feat: filter out runes utxos, closes #5207
Browse files Browse the repository at this point in the history
  • Loading branch information
alter-eggo committed Apr 17, 2024
1 parent 1858348 commit 124b431
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 4 deletions.
41 changes: 40 additions & 1 deletion src/app/query/bitcoin/address/utxos-by-address.hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/account

import { UtxoResponseItem, UtxoWithDerivationPath } from '../bitcoin-client';
import { useInscriptionsByAddressQuery } from '../ordinals/inscriptions.query';
import { useRunesEnabled, useRunesOutputsByAddress } from '../runes/runes.hooks';
import { useBitcoinPendingTransactionsInputs } from './transactions-by-address.hooks';
import { useGetUtxosByAddressQuery } from './utxos-by-address.query';

Expand All @@ -24,14 +25,15 @@ export function filterUtxosWithInscriptions(
const defaultArgs = {
filterInscriptionUtxos: true,
filterPendingTxsUtxos: true,
filterRunesUtxos: true,
};

/**
* Warning: ⚠️ To avoid spending inscriptions, when using UTXOs
* we set `filterInscriptionUtxos` and `filterPendingTxsUtxos` to true
*/
export function useCurrentNativeSegwitUtxos(args = defaultArgs) {
const { filterInscriptionUtxos, filterPendingTxsUtxos } = args;
const { filterInscriptionUtxos, filterPendingTxsUtxos, filterRunesUtxos } = args;

const nativeSegwitSigner = useCurrentAccountNativeSegwitIndexZeroSigner();
const address = nativeSegwitSigner.address;
Expand All @@ -40,13 +42,15 @@ export function useCurrentNativeSegwitUtxos(args = defaultArgs) {
address,
filterInscriptionUtxos,
filterPendingTxsUtxos,
filterRunesUtxos,
});
}

interface UseFilterUtxosByAddressArgs {
address: string;
filterInscriptionUtxos: boolean;
filterPendingTxsUtxos: boolean;
filterRunesUtxos: boolean;
}

type filterUtxoFunctionType = (utxos: UtxoResponseItem[]) => UtxoResponseItem[];
Expand All @@ -55,10 +59,12 @@ export function useNativeSegwitUtxosByAddress({
address,
filterInscriptionUtxos,
filterPendingTxsUtxos,
filterRunesUtxos,
}: UseFilterUtxosByAddressArgs) {
const { filterOutInscriptions, isInitialLoadingInscriptions } =
useFilterInscriptionsByAddress(address);
const { filterOutPendingTxsUtxos, isInitialLoading } = useFilterPendingUtxosByAddress(address);
const { filterOutRunesUtxos } = useFilterRuneUtxosByAddress(address);

const utxosQuery = useGetUtxosByAddressQuery(address, {
select(utxos) {
Expand All @@ -71,6 +77,10 @@ export function useNativeSegwitUtxosByAddress({
filters.push(filterOutInscriptions);
}

if (filterRunesUtxos) {
filters.push(filterOutRunesUtxos);
}

return filters.reduce(
(filteredUtxos: UtxoResponseItem[], filterFunc: filterUtxoFunctionType) =>
filterFunc(filteredUtxos),
Expand Down Expand Up @@ -113,6 +123,35 @@ function useFilterInscriptionsByAddress(address: string) {
};
}

function useFilterRuneUtxosByAddress(address: string) {
const { data, isLoading } = useRunesOutputsByAddress(address);
const runesEnabled = useRunesEnabled();

const filterOutRunesUtxos = useCallback(
(utxos: UtxoResponseItem[]) => {
return utxos.filter(utxo => {
// If Runes are not enabled, return all utxos
if (!runesEnabled) return true;

// To-DO If there are no runes response, filter out all utxos ?
if (!data) return false;

const hasRuneOutput = data.find(rune => {
return rune.output === `${utxo.txid}:${utxo.vout}`;
});

return !hasRuneOutput;
});
},
[data]

Check failure on line 146 in src/app/query/bitcoin/address/utxos-by-address.hooks.ts

View workflow job for this annotation

GitHub Actions / lint-eslint

React Hook useCallback has a missing dependency: 'runesEnabled'. Either include it or remove the dependency array
);

return {
filterOutRunesUtxos,
isLoading,
};
}

function useFilterPendingUtxosByAddress(address: string) {
const { data: pendingInputs = [], isInitialLoading } =
useBitcoinPendingTransactionsInputs(address);
Expand Down
4 changes: 4 additions & 0 deletions src/app/query/bitcoin/balance/btc-balance.hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ import { isUndefined } from '@shared/utils';
import { sumNumbers } from '@app/common/math/helpers';

import { useNativeSegwitUtxosByAddress } from '../address/utxos-by-address.hooks';
import { useRunesEnabled } from '../runes/runes.hooks';

export function useGetBitcoinBalanceByAddress(address: string) {
const runesEnabled = useRunesEnabled();

const {
data: utxos,
isInitialLoading,
Expand All @@ -18,6 +21,7 @@ export function useGetBitcoinBalanceByAddress(address: string) {
address,
filterInscriptionUtxos: true,
filterPendingTxsUtxos: true,
filterRunesUtxos: runesEnabled,
});

const balance = useMemo(() => {
Expand Down
58 changes: 58 additions & 0 deletions src/app/query/bitcoin/bitcoin-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,21 @@ export interface RuneToken extends RuneBalance {
balance: Money;
}

export interface RunesOutputsByAddress {
pkscript: string;
wallet_addr: string;
output: string;
rune_ids: string[];
balances: number[];
rune_names: string[];
spaced_rune_names: string[];
}

interface RunesOutputsByAddressResponse {
block_height: number;
data: RunesOutputsByAddress[];
}

class BestinslotApi {
url = BESTINSLOT_API_BASE_URL_MAINNET;
testnetUrl = BESTINSLOT_API_BASE_URL_TESTNET;
Expand Down Expand Up @@ -181,6 +196,49 @@ class BestinslotApi {
);
return resp.data.data;
}

async getRunesOutputsInfo(outputs: string[]) {
const resp = await axios.post<RunesOutputsByAddressResponse>(
`${this.testnetUrl}/runes/output_info`,
{ queries: outputs },
{ ...this.defaultOptions }
);
return resp.data.data;
}

/**
* @see https://docs.bestinslot.xyz/reference/api-reference/ordinals-and-brc-20-and-runes-and-bitmap-v3-api-mainnet+testnet/runes#runes-wallet-valid-outputs
*/
async getRunesOutputsByAddress({
address,
network = 'mainnet',
sortBy = 'output',
order = 'asc',
offset = 0,
count = 100,
}: {
address: string;
network?: BitcoinNetworkModes;
sortBy?: 'output';
order?: 'asc' | 'desc';
offset?: number;
count?: number;
}) {
const baseUrl = network === 'mainnet' ? this.url : this.testnetUrl;
const queryParams = new URLSearchParams({
address,
sort_by: sortBy,
order,
offset: offset.toString(),
count: count.toString(),
});

const resp = await axios.get<RunesOutputsByAddressResponse>(
`${baseUrl}/runes/wallet_valid_outputs?${queryParams}`,
{ ...this.defaultOptions }
);
return resp.data.data;
}
}

class HiroApi {
Expand Down
29 changes: 29 additions & 0 deletions src/app/query/bitcoin/runes/runes-outputs-by-address.query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useQuery } from '@tanstack/react-query';

import type { AppUseQueryConfig } from '@app/query/query-config';
import { useBitcoinClient } from '@app/store/common/api-clients.hooks';
import { useCurrentNetwork } from '@app/store/networks/networks.selectors';

import type { RunesOutputsByAddress } from '../bitcoin-client';
import { useRunesEnabled } from './runes.hooks';

export function useGetRunesOutputsByAddressQuery<T extends unknown = RunesOutputsByAddress[]>(
address: string,
options?: AppUseQueryConfig<RunesOutputsByAddress[], T>
) {
const client = useBitcoinClient();
const runesEnabled = useRunesEnabled();
const network = useCurrentNetwork();

return useQuery({
queryKey: ['runes-outputs-by-address', address],
queryFn: () =>
client.BestinslotApi.getRunesOutputsByAddress({
address,
network: network.chain.bitcoin.bitcoinNetwork,
}),
staleTime: 1000 * 60,
enabled: !!address && (network.chain.bitcoin.bitcoinNetwork === 'testnet' || runesEnabled),
...options,
});
}
15 changes: 15 additions & 0 deletions src/app/query/bitcoin/runes/runes.hooks.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { createMoney } from '@shared/models/money.model';

import { useConfigRunesEnabled } from '@app/query/common/remote-config/remote-config.query';
import { useCurrentNetwork } from '@app/store/networks/networks.selectors';

import type { RuneBalance, RuneToken } from '../bitcoin-client';
import { useGetRunesOutputsByAddressQuery } from './runes-outputs-by-address.query';
import { useGetRunesWalletBalancesByAddressesQuery } from './runes-wallet-balances.query';

function makeRuneToken(rune: RuneBalance): RuneToken {
Expand All @@ -10,8 +14,19 @@ function makeRuneToken(rune: RuneBalance): RuneToken {
};
}

export function useRunesEnabled() {
const runesEnabled = useConfigRunesEnabled();
const network = useCurrentNetwork();

return runesEnabled || network.chain.bitcoin.bitcoinNetwork === 'testnet';
}

export function useRuneTokens(addresses: string[]) {
return useGetRunesWalletBalancesByAddressesQuery(addresses, {
select: resp => resp.map(makeRuneToken),
});
}

export function useRunesOutputsByAddress(address: string) {
return useGetRunesOutputsByAddressQuery(address);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { delay, isError } from '@shared/utils';
import { useAnalytics } from '@app/common/hooks/analytics/use-analytics';
import { useBitcoinClient } from '@app/store/common/api-clients.hooks';

import { filterOutIntentionalUtxoSpend, useCheckInscribedUtxos } from './use-check-utxos';
import { filterOutIntentionalUtxoSpend, useCheckUnspendableUtxos } from './use-check-utxos';

interface BroadcastCallbackArgs {
tx: string;
Expand All @@ -23,7 +23,7 @@ export function useBitcoinBroadcastTransaction() {
const client = useBitcoinClient();
const [isBroadcasting, setIsBroadcasting] = useState(false);
const analytics = useAnalytics();
const { checkIfUtxosListIncludesInscribed } = useCheckInscribedUtxos();
const { checkIfUtxosListIncludesInscribed } = useCheckUnspendableUtxos();

const broadcastTx = useCallback(
async ({
Expand Down
2 changes: 1 addition & 1 deletion src/app/query/bitcoin/transaction/use-check-utxos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ async function checkInscribedUtxosByBestinslot({
return hasInscribedUtxos;
}

export function useCheckInscribedUtxos(blockTxAction?: () => void) {
export function useCheckUnspendableUtxos(blockTxAction?: () => void) {
const client = useBitcoinClient();
const analytics = useAnalytics();
const [isLoading, setIsLoading] = useState(false);
Expand Down

0 comments on commit 124b431

Please sign in to comment.