Skip to content

Commit

Permalink
feat: balances service
Browse files Browse the repository at this point in the history
  • Loading branch information
alexp3y committed Dec 2, 2024
1 parent 44fd6cf commit 052f083
Show file tree
Hide file tree
Showing 22 changed files with 517 additions and 190 deletions.
53 changes: 22 additions & 31 deletions apps/mobile/src/queries/balance/bitcoin-balance.query.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import { useCallback } from 'react';

import { AccountId } from '@/models/domain.model';
import { useBitcoinAccounts } from '@/store/keychains/bitcoin/bitcoin-keychains.read';
import { useSettings } from '@/store/settings/settings';
import { UseQueryResult, useQueries } from '@tanstack/react-query';
import { QueryFunctionContext, useQueries, useQuery } from '@tanstack/react-query';

import { inferPaymentTypeFromPath } from '@leather.io/bitcoin';
import { Utxo, createUtxoQueryOptions } from '@leather.io/query';
import { baseCurrencyAmountInQuote, createMoney, isDefined, sumNumbers } from '@leather.io/utils';

import { useBtcMarketDataQuery } from '../market-data/btc-market-data.query';
import { getBitcoinBalancesService } from '@leather.io/services';
import { createMoney, isDefined } from '@leather.io/utils';

function getDescriptorFromKeychain<T extends { keyOrigin: string; xpub: string }>(
accountKeychain: T
Expand Down Expand Up @@ -57,32 +54,12 @@ export function useBitcoinAccountUtxos({ fingerprint, accountIndex }: AccountId)
});
}

type TotalBalanceCombineFn = UseQueryResult<Utxo[], Error>[];

export function useTotalBitcoinBalanceOfDescriptors(descriptors: string[]) {
const queries = useCreateBitcoinAccountUtxoQueryOptions(descriptors);
const { data: btcMarketData } = useBtcMarketDataQuery();

const combine = useCallback(
(results: TotalBalanceCombineFn) => {
const amount = sumNumbers(
results
.map(data => data.data)
.filter(isDefined)
.map(data => sumNumbers(data.map(utxo => Number(utxo.value))).toNumber())
);

const availableBalance = createMoney(amount, 'BTC');
const fiatBalance = btcMarketData
? baseCurrencyAmountInQuote(availableBalance, btcMarketData)
: createMoney(0, 'USD');

return { availableBalance, fiatBalance };
},
[btcMarketData]
);

return useQueries({ queries, combine });
const { isLoading, data } = useBtcBalanceQuery(descriptors);
return {
availableBalance: !isLoading && data ? data.balanceBtc.availableBalance : createMoney(0, 'BTC'),
fiatBalance: !isLoading && data ? data.balanceUsd.availableBalance : createMoney(0, 'USD'),
};
}

export function useWalletTotalBitcoinBalance() {
Expand All @@ -94,3 +71,17 @@ export function useBitcoinAccountTotalBitcoinBalance({ fingerprint, accountIndex
const descriptors = useBitcoinDescriptorsByAcccount(fingerprint, accountIndex);
return useTotalBitcoinBalanceOfDescriptors(descriptors);
}

export function useBtcBalanceQuery(descriptors: string[]) {
return useQuery({
queryKey: ['bitcoin-balance-service-get-btc-balance', descriptors],
queryFn: ({ signal }: QueryFunctionContext) =>
getBitcoinBalancesService().getBtcBalance(descriptors, signal),
refetchOnReconnect: false,
refetchOnWindowFocus: false,
refetchOnMount: true,
retryOnMount: false,
staleTime: 1 * 1000,
gcTime: 1 * 1000,
});
}
9 changes: 6 additions & 3 deletions apps/mobile/src/queries/balance/stacks-balance.query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ import {
createGetStacksAccountBalanceQueryOptions,
createStxCryptoAssetBalance,
createStxMoney,
useCryptoCurrencyMarketDataMeanAverage,
useCurrentNetworkState,
useStacksClient,
} from '@leather.io/query';
import { baseCurrencyAmountInQuote, createMoney, sumMoney } from '@leather.io/utils';

import { useStxMarketDataQuery } from '../market-data/stx-market-data.query';

interface StxBalances {
totalStxBalance: Money;
}
Expand Down Expand Up @@ -68,7 +69,9 @@ function useStxBalancesQueries(addresses: string[]) {
export function useStxBalance(addresses: string[]) {
const { totalStxBalance } = useGetStxBalanceByAddresses(addresses);

const stxMarketData = useCryptoCurrencyMarketDataMeanAverage('STX');
const stxBalanceUsd = baseCurrencyAmountInQuote(totalStxBalance, stxMarketData);
const { data: stxMarketData } = useStxMarketDataQuery();
const stxBalanceUsd = stxMarketData
? baseCurrencyAmountInQuote(totalStxBalance, stxMarketData)
: createMoney(0, 'USD');
return { availableBalance: totalStxBalance, fiatBalance: stxBalanceUsd };
}
38 changes: 17 additions & 21 deletions packages/models/src/crypto-assets/crypto-asset-balance.model.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
import { Money } from '../money.model';

export interface BaseCryptoAssetBalance {
/**
* Balance as confirmed on chain
*/
readonly totalBalance: Money;
/**
* Balance of pending receipt into account given pending transactions
*/
readonly inboundBalance: Money;
/**
* Balance of pending delivery from account given pending transactions
*/
readonly outboundBalance: Money;
/**
* totalBalance plus inboundBalance minus outboundBalance
*/
readonly pendingBalance: Money;
/**
* totalBalance after filtering out outboundBalance, protectedBalance, and uneconomicalBalance
*/
Expand All @@ -24,25 +40,9 @@ export interface StxCryptoAssetBalance extends BaseCryptoAssetBalance {
*/
readonly availableUnlockedBalance: Money;
/**
* Balance of pending receipt into account given pending transactions
*/
readonly inboundBalance: Money;
/**
* totalBalance minus total amount locked by contracts
* total amount locked by contracts
*/
readonly lockedBalance: Money;
/**
* Balance of pending delivery from account given pending transactions
*/
readonly outboundBalance: Money;
/**
* totalBalance plus inboundBalance minus outboundBalance
*/
readonly pendingBalance: Money;
/**
* Balance as confirmed on chain
*/
readonly totalBalance: Money;
/**
* totalBalance minus lockedBalance
*/
Expand All @@ -53,7 +53,3 @@ export type CryptoAssetBalance =
| BaseCryptoAssetBalance
| BtcCryptoAssetBalance
| StxCryptoAssetBalance;

export function createCryptoAssetBalance(balance: Money): BaseCryptoAssetBalance {
return { availableBalance: balance };
}
5 changes: 2 additions & 3 deletions packages/query/src/bitcoin/runes/runes.utils.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import {
Money,
type RuneCryptoAssetInfo,
createCryptoAssetBalance,
createMarketData,
createMarketPair,
} from '@leather.io/models';
import { createMoney } from '@leather.io/utils';
import { createBaseCryptoAssetBalance, createMoney } from '@leather.io/utils';

import { RuneBalance, RuneTickerInfo } from '../clients/best-in-slot';

Expand All @@ -30,7 +29,7 @@ export function createRuneCryptoAssetDetails(
fiatPrice: Money
) {
return {
balance: createCryptoAssetBalance(
balance: createBaseCryptoAssetBalance(

Check warning on line 32 in packages/query/src/bitcoin/runes/runes.utils.ts

View check run for this annotation

Codecov / codecov/patch

packages/query/src/bitcoin/runes/runes.utils.ts#L32

Added line #L32 was not covered by tests
createMoney(Number(runeBalance.total_balance), tickerInfo.rune_name, tickerInfo.decimals)
),
info: createRuneCryptoAssetInfo(tickerInfo),
Expand Down
5 changes: 2 additions & 3 deletions packages/query/src/bitcoin/stamps/stamps-by-address.hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@ import {
CryptoAssetChains,
CryptoAssetProtocols,
Src20CryptoAssetInfo,
createCryptoAssetBalance,
} from '@leather.io/models';
import { createMoney } from '@leather.io/utils';
import { createBaseCryptoAssetBalance, createMoney } from '@leather.io/utils';

import { Src20Token, createGetStampsByAddressQueryOptions } from './stamps-by-address.query';

Expand Down Expand Up @@ -36,7 +35,7 @@ export function useSrc20TokensByAddress(address: string) {
...createGetStampsByAddressQueryOptions(address),
select: resp =>
resp.data.src20.map(token => ({
balance: createCryptoAssetBalance(
balance: createBaseCryptoAssetBalance(

Check warning on line 38 in packages/query/src/bitcoin/stamps/stamps-by-address.hooks.ts

View check run for this annotation

Codecov / codecov/patch

packages/query/src/bitcoin/stamps/stamps-by-address.hooks.ts#L38

Added line #L38 was not covered by tests
createMoney(new BigNumber(token.amt ?? 0), token.tick, 0)
),
info: createSrc20CryptoAssetInfo(token),
Expand Down
6 changes: 3 additions & 3 deletions packages/query/src/stacks/stx20/stx20-tokens.hooks.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { useQuery } from '@tanstack/react-query';
import BigNumber from 'bignumber.js';

import { Stx20CryptoAssetInfo, createCryptoAssetBalance } from '@leather.io/models';
import { createMoney } from '@leather.io/utils';
import { Stx20CryptoAssetInfo } from '@leather.io/models';
import { createBaseCryptoAssetBalance, createMoney } from '@leather.io/utils';

import { useCurrentNetworkState } from '../../leather-query-provider';
import { useStacksClient } from '../stacks-client';
Expand Down Expand Up @@ -30,7 +30,7 @@ export function useStx20Tokens(address: string) {
}),
select: resp =>
resp.map(stx20Balance => ({
balance: createCryptoAssetBalance(
balance: createBaseCryptoAssetBalance(

Check warning on line 33 in packages/query/src/stacks/stx20/stx20-tokens.hooks.ts

View check run for this annotation

Codecov / codecov/patch

packages/query/src/stacks/stx20/stx20-tokens.hooks.ts#L33

Added line #L33 was not covered by tests
createMoney(new BigNumber(stx20Balance.balance), stx20Balance.ticker, 0)
),
info: createStx20CryptoAssetInfo(stx20Balance),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { useQueries } from '@tanstack/react-query';
import BigNumber from 'bignumber.js';

import { createCryptoAssetBalance } from '@leather.io/models';
import { createMoney, getPrincipalFromContractId, getTicker } from '@leather.io/utils';
import {
createBaseCryptoAssetBalance,
createMoney,
getPrincipalFromContractId,
getTicker,
} from '@leather.io/utils';

import { useCurrentNetworkState } from '../../../leather-query-provider';
import { AddressBalanceResponse } from '../../hiro-api-types';
Expand Down Expand Up @@ -34,7 +38,7 @@ export function useStacksFungibleTokensBalance(
const symbol = resp.symbol || getTicker(name);
return {
contractId: key,
balance: createCryptoAssetBalance(
balance: createBaseCryptoAssetBalance(

Check warning on line 41 in packages/query/src/stacks/token-metadata/fungible-tokens/fungible-token-metadata.hooks.ts

View check run for this annotation

Codecov / codecov/patch

packages/query/src/stacks/token-metadata/fungible-tokens/fungible-token-metadata.hooks.ts#L41

Added line #L41 was not covered by tests
createMoney(new BigNumber(value.balance), symbol, resp.decimals ?? 0)
),
};
Expand Down
2 changes: 2 additions & 0 deletions packages/services/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"bugs": "https://github.com/leather-io/mono/issues",
"dependencies": {
"@leather.io/bitcoin": "workspace:*",
"@leather.io/constants": "workspace:*",
"@leather.io/models": "workspace:*",
"@leather.io/utils": "workspace:*",
"alex-sdk": "2.1.4",
Expand All @@ -41,6 +42,7 @@
"@leather.io/prettier-config": "workspace:*",
"@leather.io/rpc": "workspace:*",
"@leather.io/tsconfig-config": "workspace:*",
"@stacks/stacks-blockchain-api-types": "7.8.2",
"eslint": "8.53.0",
"prettier": "3.3.3",
"tslib": "2.6.2",
Expand Down
101 changes: 101 additions & 0 deletions packages/services/src/balances/bitcoin-balances.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { BtcCryptoAssetBalance } from '@leather.io/models';

Check warning on line 1 in packages/services/src/balances/bitcoin-balances.service.ts

View check run for this annotation

Codecov / codecov/patch

packages/services/src/balances/bitcoin-balances.service.ts#L1

Added line #L1 was not covered by tests
import {
baseCurrencyAmountInQuote,
createBtcCryptoAssetBalance,
createMoney,
isDefined,
sumNumbers,
} from '@leather.io/utils';

import { LeatherApiClient, LeatherApiUtxo } from '../infrastructure/api/leather/leather-api.client';
import { MarketDataService } from '../market-data/market-data.service';
import { hasUneconomicalBalance, removeExistingUtxos } from './bitcoin-balances.utils';

export interface BtcBalance {
balanceBtc: BtcCryptoAssetBalance;
balanceUsd: BtcCryptoAssetBalance;
}

export interface BitcoinBalancesService {
getBtcBalance(descriptors: string[], signal?: AbortSignal): Promise<BtcBalance>;
}

export function createBitcoinBalancesService(
leatherApiClient: LeatherApiClient,
marketDataService: MarketDataService
): BitcoinBalancesService {

Check warning on line 26 in packages/services/src/balances/bitcoin-balances.service.ts

View check run for this annotation

Codecov / codecov/patch

packages/services/src/balances/bitcoin-balances.service.ts#L23-L26

Added lines #L23 - L26 were not covered by tests
/**
* Retrieves total BTC balance for listed descriptors. Includes all sub-balances (inbound, outbound, protected, uneconomical).
*
* @returns {BtcBalance} BTC balance denominanted in both in BTC and fiat (USD).
*/
async function getBtcBalance(descriptors: string[], signal?: AbortSignal) {
const utxoPromises = descriptors.map(descriptor =>
leatherApiClient.fetchUtxos(descriptor, signal)
);
const totalUtxos = (await Promise.all(utxoPromises)).flat().filter(isDefined);
const totalBalanceBtc = createMoney(
sumNumbers(totalUtxos.map(utxo => Number(utxo.value))).toNumber(),
'BTC'
);

Check warning on line 40 in packages/services/src/balances/bitcoin-balances.service.ts

View check run for this annotation

Codecov / codecov/patch

packages/services/src/balances/bitcoin-balances.service.ts#L32-L40

Added lines #L32 - L40 were not covered by tests
// TODO: fetch inbound utxos
const inboundUtxos = await getInboundUtxos(descriptors);
const inboundBalanceBtc = createMoney(
sumNumbers(inboundUtxos.map(utxo => Number(utxo.value))).toNumber(),
'BTC'
);

Check warning on line 46 in packages/services/src/balances/bitcoin-balances.service.ts

View check run for this annotation

Codecov / codecov/patch

packages/services/src/balances/bitcoin-balances.service.ts#L42-L46

Added lines #L42 - L46 were not covered by tests
// TODO: fetch outbound utxos
const outboundUtxos = await getOutboundUtxos(descriptors);
const outboundBalanceBtc = createMoney(
sumNumbers(outboundUtxos.map(utxo => Number(utxo.value))).toNumber(),
'BTC'
);

Check warning on line 52 in packages/services/src/balances/bitcoin-balances.service.ts

View check run for this annotation

Codecov / codecov/patch

packages/services/src/balances/bitcoin-balances.service.ts#L48-L52

Added lines #L48 - L52 were not covered by tests
// TODO: fetch protected utxos
const protectedUtxos = await getProtectedUtxos(descriptors);
const protectedBalanceBtc = createMoney(
sumNumbers(protectedUtxos.map(utxo => Number(utxo.value))).toNumber(),
'BTC'
);
const uneconomicalUtxos = removeExistingUtxos(totalUtxos, protectedUtxos).filter(
hasUneconomicalBalance
);
const uneconomicalBalanceBtc = createMoney(
sumNumbers(uneconomicalUtxos.map(utxo => Number(utxo.value))).toNumber(),
'BTC'
);

Check warning on line 65 in packages/services/src/balances/bitcoin-balances.service.ts

View check run for this annotation

Codecov / codecov/patch

packages/services/src/balances/bitcoin-balances.service.ts#L54-L65

Added lines #L54 - L65 were not covered by tests

const btcMarketData = await marketDataService.getBtcMarketData(signal);
return {
balanceBtc: createBtcCryptoAssetBalance(
totalBalanceBtc,
inboundBalanceBtc,
outboundBalanceBtc,
protectedBalanceBtc,
uneconomicalBalanceBtc
),
balanceUsd: createBtcCryptoAssetBalance(
baseCurrencyAmountInQuote(totalBalanceBtc, btcMarketData),
baseCurrencyAmountInQuote(inboundBalanceBtc, btcMarketData),
baseCurrencyAmountInQuote(outboundBalanceBtc, btcMarketData),
baseCurrencyAmountInQuote(protectedBalanceBtc, btcMarketData),
baseCurrencyAmountInQuote(uneconomicalBalanceBtc, btcMarketData)
),
};
}

Check warning on line 84 in packages/services/src/balances/bitcoin-balances.service.ts

View check run for this annotation

Codecov / codecov/patch

packages/services/src/balances/bitcoin-balances.service.ts#L67-L84

Added lines #L67 - L84 were not covered by tests

async function getInboundUtxos(_descriptors: string[]): Promise<LeatherApiUtxo[]> {
return [];
}

Check warning on line 88 in packages/services/src/balances/bitcoin-balances.service.ts

View check run for this annotation

Codecov / codecov/patch

packages/services/src/balances/bitcoin-balances.service.ts#L86-L88

Added lines #L86 - L88 were not covered by tests

async function getOutboundUtxos(_descriptors: string[]): Promise<LeatherApiUtxo[]> {
return [];
}

Check warning on line 92 in packages/services/src/balances/bitcoin-balances.service.ts

View check run for this annotation

Codecov / codecov/patch

packages/services/src/balances/bitcoin-balances.service.ts#L90-L92

Added lines #L90 - L92 were not covered by tests

async function getProtectedUtxos(_descriptors: string[]): Promise<LeatherApiUtxo[]> {
return [];
}

Check warning on line 96 in packages/services/src/balances/bitcoin-balances.service.ts

View check run for this annotation

Codecov / codecov/patch

packages/services/src/balances/bitcoin-balances.service.ts#L94-L96

Added lines #L94 - L96 were not covered by tests

return {
getBtcBalance,
};
}

Check warning on line 101 in packages/services/src/balances/bitcoin-balances.service.ts

View check run for this annotation

Codecov / codecov/patch

packages/services/src/balances/bitcoin-balances.service.ts#L98-L101

Added lines #L98 - L101 were not covered by tests
16 changes: 16 additions & 0 deletions packages/services/src/balances/bitcoin-balances.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { LeatherApiUtxo } from '../infrastructure/api/leather/leather-api.client';

Check warning on line 1 in packages/services/src/balances/bitcoin-balances.utils.ts

View check run for this annotation

Codecov / codecov/patch

packages/services/src/balances/bitcoin-balances.utils.ts#L1

Added line #L1 was not covered by tests

export const uneconomicalSatThreshold = 10000;

Check warning on line 3 in packages/services/src/balances/bitcoin-balances.utils.ts

View check run for this annotation

Codecov / codecov/patch

packages/services/src/balances/bitcoin-balances.utils.ts#L3

Added line #L3 was not covered by tests

export function hasUneconomicalBalance(utxo: LeatherApiUtxo) {
return Number(utxo.value) < uneconomicalSatThreshold;
}

Check warning on line 7 in packages/services/src/balances/bitcoin-balances.utils.ts

View check run for this annotation

Codecov / codecov/patch

packages/services/src/balances/bitcoin-balances.utils.ts#L5-L7

Added lines #L5 - L7 were not covered by tests

export function removeExistingUtxos(
candidateUtxos: LeatherApiUtxo[],
existingUtxos: LeatherApiUtxo[]
) {
const existingUtxoIds = new Set(existingUtxos.map(utxo => `${utxo.txid}:${utxo.vout}`));

Check warning on line 13 in packages/services/src/balances/bitcoin-balances.utils.ts

View check run for this annotation

Codecov / codecov/patch

packages/services/src/balances/bitcoin-balances.utils.ts#L9-L13

Added lines #L9 - L13 were not covered by tests

return candidateUtxos.filter(utxo => !existingUtxoIds.has(`${utxo.txid}:${utxo.vout}`));
}

Check warning on line 16 in packages/services/src/balances/bitcoin-balances.utils.ts

View check run for this annotation

Codecov / codecov/patch

packages/services/src/balances/bitcoin-balances.utils.ts#L15-L16

Added lines #L15 - L16 were not covered by tests
3 changes: 2 additions & 1 deletion packages/services/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from './inversify.config';
export * from './infrastructure/settings/network-settings.service';
export * from './infrastructure/api/alex-sdk/alex-sdk.client';
export * from './infrastructure/api/coingecko/coingecko-api.client';
export * from './market-data/market-data.service';
export * from './infrastructure/cache/http-cache.service';
export * from './infrastructure/cache/http-cache.utils';
export * from './market-data/market-data.service';
export * from './balances/bitcoin-balances.service';

Check warning on line 8 in packages/services/src/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/services/src/index.ts#L7-L8

Added lines #L7 - L8 were not covered by tests
Loading

0 comments on commit 052f083

Please sign in to comment.