Skip to content

Commit

Permalink
feat: stx-20 balances, closes #5077
Browse files Browse the repository at this point in the history
  • Loading branch information
fbwoolf committed Apr 30, 2024
1 parent d12bb82 commit e23c1a1
Show file tree
Hide file tree
Showing 28 changed files with 215 additions and 117 deletions.
Binary file added public/assets/avatars/stx20-avatar-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import type { Src20Token } from '@app/query/bitcoin/stamps/stamps-by-address.que
import { Src20TokenAssetItemLayout } from './src20-token-asset-item.layout';

interface Src20TokenAssetListProps {
src20Tokens: Src20Token[];
tokens: Src20Token[];
}
export function Src20TokenAssetList({ src20Tokens }: Src20TokenAssetListProps) {
return src20Tokens.map((token, i) => (
export function Src20TokenAssetList({ tokens }: Src20TokenAssetListProps) {
return tokens.map((token, i) => (
<Src20TokenAssetItemLayout key={`${token.id}${i}`} token={token} />
));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { styled } from 'leather-styles/jsx';

import { formatBalance } from '@app/common/format-balance';
import type { Stx20Token } from '@app/query/stacks/stacks-client';
import { Stx20AvatarIcon } from '@app/ui/components/avatar/stx20-avatar-icon';
import { ItemLayout } from '@app/ui/components/item-layout/item-layout';
import { BasicTooltip } from '@app/ui/components/tooltip/basic-tooltip';
import { Pressable } from '@app/ui/pressable/pressable';

interface Stx20TokenAssetItemLayoutProps {
token: Stx20Token;
}
export function Stx20TokenAssetItemLayout({ token }: Stx20TokenAssetItemLayoutProps) {
const balanceAsString = token.balance.amount.toString();
const formattedBalance = formatBalance(balanceAsString);

return (
<Pressable my="space.02">
<ItemLayout
flagImg={<Stx20AvatarIcon />}
titleLeft={token.tokenData.ticker}
captionLeft="STX-20"
titleRight={
<BasicTooltip
asChild
label={formattedBalance.isAbbreviated ? balanceAsString : undefined}
side="left"
>
<styled.span data-testid={token.tokenData.ticker} fontWeight={500} textStyle="label.02">
{formattedBalance.value}
</styled.span>
</BasicTooltip>
}
/>
</Pressable>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { Stx20Token } from '@app/query/stacks/stacks-client';

import { Stx20TokenAssetItemLayout } from './stx20-token-asset-item.layout';

interface Stx20TokenAssetListProps {
tokens: Stx20Token[];
}
export function Stx20TokenAssetList({ tokens }: Stx20TokenAssetListProps) {
return tokens.map((token, i) => (
<Stx20TokenAssetItemLayout key={`${token.tokenData.ticker}${i}`} token={token} />
));
}
6 changes: 3 additions & 3 deletions src/app/components/loaders/brc20-tokens-loader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { Brc20Token } from '@app/query/bitcoin/bitcoin-client';
import { useBrc20Tokens } from '@app/query/bitcoin/ordinals/brc20/brc20-tokens.hooks';

interface Brc20TokensLoaderProps {
children(brc20Tokens: Brc20Token[]): React.ReactNode;
children(tokens: Brc20Token[]): React.ReactNode;
}
export function Brc20TokensLoader({ children }: Brc20TokensLoaderProps) {
const brc20Tokens = useBrc20Tokens();
return children(brc20Tokens);
const tokens = useBrc20Tokens();
return children(tokens);
}
6 changes: 3 additions & 3 deletions src/app/components/loaders/src20-tokens-loader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import type { Src20Token } from '@app/query/bitcoin/stamps/stamps-by-address.que

interface Src20TokensLoaderProps {
address: string;
children(src20Tokens: Src20Token[]): React.ReactNode;
children(tokens: Src20Token[]): React.ReactNode;
}
export function Src20TokensLoader({ address, children }: Src20TokensLoaderProps) {
const { data: src20Tokens = [] } = useSrc20TokensByAddress(address);
return children(src20Tokens);
const { data: tokens = [] } = useSrc20TokensByAddress(address);
return children(tokens);
}
11 changes: 11 additions & 0 deletions src/app/components/loaders/stx20-tokens-loader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { Stx20Token } from '@app/query/stacks/stacks-client';
import { useStx20Tokens } from '@app/query/stacks/stx20/stx20-tokens.hooks';

interface Stx20TokensLoaderProps {
address: string;
children(tokens: Stx20Token[]): React.ReactNode;
}
export function Stx20TokensLoader({ address, children }: Stx20TokensLoaderProps) {
const { data: tokens = [] } = useStx20Tokens(address);
return children(tokens);
}
27 changes: 22 additions & 5 deletions src/app/features/asset-list/asset-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,16 @@ import {
BitcoinTaprootAccountLoader,
} from '@app/components/account/bitcoin-account-loader';
import { BitcoinContractEntryPoint } from '@app/components/bitcoin-contract-entry-point/bitcoin-contract-entry-point';
import { Brc20TokenAssetList } from '@app/components/crypto-assets/bitcoin/brc20-token-asset-list/brc20-token-asset-list';
import { RunesAssetList } from '@app/components/crypto-assets/bitcoin/runes-asset-list/runes-asset-list';
import { Src20TokenAssetList } from '@app/components/crypto-assets/bitcoin/src20-token-asset-list/src20-token-asset-list';
import { CryptoCurrencyAssetItemLayout } from '@app/components/crypto-assets/crypto-currency-asset/crypto-currency-asset-item.layout';
import { Stx20TokenAssetList } from '@app/components/crypto-assets/stacks/stx20-token-asset-list/stx20-token-asset-list';
import { Brc20TokensLoader } from '@app/components/loaders/brc20-tokens-loader';
import { RunesLoader } from '@app/components/loaders/runes-loader';
import { Src20TokensLoader } from '@app/components/loaders/src20-tokens-loader';
import { CurrentStacksAccountLoader } from '@app/components/loaders/stacks-account-loader';
import { Stx20TokensLoader } from '@app/components/loaders/stx20-tokens-loader';
import { useHasBitcoinLedgerKeychain } from '@app/store/accounts/blockchain/bitcoin/bitcoin.ledger';
import { useCurrentAccountNativeSegwitAddressIndexZero } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import { useCurrentNetwork } from '@app/store/networks/networks.selectors';
Expand All @@ -20,7 +28,6 @@ import { BtcAvatarIcon } from '@app/ui/components/avatar/btc-avatar-icon';
import { Collectibles } from '../collectibles/collectibles';
import { PendingBrc20TransferList } from '../pending-brc-20-transfers/pending-brc-20-transfers';
import { AddStacksLedgerKeysItem } from './components/add-stacks-ledger-keys-item';
import { BitcoinFungibleTokenAssetList } from './components/bitcoin-fungible-tokens-asset-list';
import { ConnectLedgerAssetBtn } from './components/connect-ledger-asset-button';
import { StacksBalanceListItem } from './components/stacks-balance-list-item';
import { StacksFungibleTokenAssetList } from './components/stacks-fungible-token-asset-list';
Expand Down Expand Up @@ -74,6 +81,9 @@ export function AssetsList() {
<>
<StacksBalanceListItem address={account.address} />
<StacksFungibleTokenAssetList address={account.address} />
<Stx20TokensLoader address={account.address}>
{tokens => <Stx20TokenAssetList tokens={tokens} />}
</Stx20TokensLoader>
</>
)}
</CurrentStacksAccountLoader>
Expand All @@ -82,10 +92,17 @@ export function AssetsList() {
{nativeSegwitAccount => (
<BitcoinTaprootAccountLoader current>
{taprootAccount => (
<BitcoinFungibleTokenAssetList
btcAddressNativeSegwit={nativeSegwitAccount.address}
btcAddressTaproot={taprootAccount.address}
/>
<>
<Brc20TokensLoader>
{tokens => <Brc20TokenAssetList tokens={tokens} />}
</Brc20TokensLoader>
<Src20TokensLoader address={nativeSegwitAccount.address}>
{tokens => <Src20TokenAssetList tokens={tokens} />}
</Src20TokensLoader>
<RunesLoader addresses={[nativeSegwitAccount.address, taprootAccount.address]}>
{runes => <RunesAssetList runes={runes} />}
</RunesLoader>
</>
)}
</BitcoinTaprootAccountLoader>
)}
Expand Down

This file was deleted.

10 changes: 5 additions & 5 deletions src/app/query/bitcoin/bitcoin-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ interface RunesOutputsByAddressResponse {
data: RunesOutputsByAddress[];
}

class BestinslotApi {
class BestinSlotApi {
url = BESTINSLOT_API_BASE_URL_MAINNET;
testnetUrl = BESTINSLOT_API_BASE_URL_TESTNET;

Expand Down Expand Up @@ -407,13 +407,13 @@ export class BitcoinClient {
addressApi: AddressApi;
feeEstimatesApi: FeeEstimatesApi;
transactionsApi: TransactionsApi;
BestinslotApi: BestinslotApi;
bestinSlotApi: BestinSlotApi;

constructor(basePath: string) {
this.configuration = new Configuration(basePath);
constructor(baseUrl: string) {
this.configuration = new Configuration(baseUrl);
this.addressApi = new AddressApi(this.configuration);
this.feeEstimatesApi = new FeeEstimatesApi(this.configuration);
this.transactionsApi = new TransactionsApi(this.configuration);
this.BestinslotApi = new BestinslotApi(this.configuration);
this.bestinSlotApi = new BestinSlotApi(this.configuration);
}
}
5 changes: 4 additions & 1 deletion src/app/query/bitcoin/ordinals/brc20/brc20-tokens.hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ import { brc20TransferInitiated } from '@app/store/ordinals/ordinals.slice';
import type { Brc20Token } from '../../bitcoin-client';
import { useAverageBitcoinFeeRates } from '../../fees/fee-estimates.hooks';
import { useOrdinalsbotClient } from '../../ordinalsbot-client';
import { createBrc20TransferInscription, encodeBrc20TransferInscription } from './brc-20.utils';
import {
createBrc20TransferInscription,
encodeBrc20TransferInscription,
} from './brc20-tokens.utils';

// ts-unused-exports:disable-next-line
export function useBrc20FeatureFlag() {
Expand Down
4 changes: 2 additions & 2 deletions src/app/query/bitcoin/ordinals/brc20/brc20-tokens.query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,11 @@ export function useGetBrc20TokensQuery() {
}

const brc20TokensPromises = addressesData.map(async address => {
const brc20Tokens = await client.BestinslotApi.getBrc20Balances(address);
const brc20Tokens = await client.bestinSlotApi.getBrc20Balances(address);

const tickerPromises = await Promise.all(
brc20Tokens.data.map(token => {
return client.BestinslotApi.getBrc20TickerInfo(token.ticker);
return client.bestinSlotApi.getBrc20TickerInfo(token.ticker);
})
);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { encodeBrc20TransferInscription } from './brc-20.utils';
import { encodeBrc20TransferInscription } from './brc20-tokens.utils';

describe(encodeBrc20TransferInscription.name, () => {
test('that it encodes the BRC-20 transfer correctly', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function useGetRunesOutputsByAddressQuery<T extends unknown = RunesOutput
enabled: !!address && runesEnabled,
queryKey: ['runes-outputs-by-address', address],
queryFn: () =>
client.BestinslotApi.getRunesOutputsByAddress({
client.bestinSlotApi.getRunesOutputsByAddress({
address,
network: network.chain.bitcoin.bitcoinNetwork,
}),
Expand Down
2 changes: 1 addition & 1 deletion src/app/query/bitcoin/runes/runes-ticker-info.query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export function useGetRunesTickerInfoQuery(runeNames: string[]): UseQueryResult<
enabled: !!runeName && (network.chain.bitcoin.bitcoinNetwork === 'testnet' || runesEnabled),
queryKey: ['runes-ticker-info', runeName],
queryFn: () =>
client.BestinslotApi.getRunesTickerInfo(runeName, network.chain.bitcoin.bitcoinNetwork),
client.bestinSlotApi.getRunesTickerInfo(runeName, network.chain.bitcoin.bitcoinNetwork),
...queryOptions,
};
}),
Expand Down
2 changes: 1 addition & 1 deletion src/app/query/bitcoin/runes/runes-wallet-balances.query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export function useGetRunesWalletBalancesByAddressesQuery<T extends unknown = Ru
enabled: !!address && (network.chain.bitcoin.bitcoinNetwork === 'testnet' || runesEnabled),
queryKey: ['runes-wallet-balances', address],
queryFn: () =>
client.BestinslotApi.getRunesWalletBalances(
client.bestinSlotApi.getRunesWalletBalances(
address,
network.chain.bitcoin.bitcoinNetwork
),
Expand Down
4 changes: 2 additions & 2 deletions src/app/query/bitcoin/transaction/use-check-utxos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,15 @@ async function checkInscribedUtxosByBestinslot({
* @see https://docs.bestinslot.xyz/reference/api-reference/ordinals-and-brc-20-and-bitmap-v3-api-mainnet+testnet/inscriptions
*/
const inscriptionIdsList = await Promise.all(
txids.map(id => client.BestinslotApi.getInscriptionsByTransactionId(id))
txids.map(id => client.bestinSlotApi.getInscriptionsByTransactionId(id))
);

const inscriptionIds = inscriptionIdsList.flatMap(inscription =>
inscription.data.map(data => data.inscription_id)
);

const inscriptionsList = await Promise.all(
inscriptionIds.map(id => client.BestinslotApi.getInscriptionById(id))
inscriptionIds.map(id => client.bestinSlotApi.getInscriptionById(id))
);

const hasInscribedUtxos = inscriptionsList.some(resp => {
Expand Down
37 changes: 36 additions & 1 deletion src/app/query/stacks/stacks-client.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { TokensApi } from '@hirosystems/token-metadata-api-client';
import {
AccountsApi,
BlocksApi,
Configuration,
ConfigurationParameters,
type ConfigurationParameters,
FaucetsApi,
FeesApi,
FungibleTokensApi,
Expand All @@ -14,6 +15,36 @@ import {
SmartContractsApi,
TransactionsApi,
} from '@stacks/blockchain-api-client';
import axios from 'axios';

import { STX20_API_BASE_URL_MAINNET } from '@shared/constants';
import type { Money } from '@shared/models/money.model';

export interface Stx20Balance {
ticker: string;
balance: string;
updateDate: string;
}

interface Stx20BalanceResponse {
address: string;
balances: Stx20Balance[];
}

export interface Stx20Token {
balance: Money;
marketData: null;
tokenData: Stx20Balance;
}

class Stx20Api {
url = STX20_API_BASE_URL_MAINNET;

async getStx20Balances(address: string) {
const resp = await axios.get<Stx20BalanceResponse>(`${this.url}/balance/${address}`);
return resp.data.balances;
}
}

export class StacksClient {
configuration: Configuration;
Expand All @@ -29,6 +60,8 @@ export class StacksClient {
rosettaApi: RosettaApi;
fungibleTokensApi: FungibleTokensApi;
nonFungibleTokensApi: NonFungibleTokensApi;
tokensApi: TokensApi;
stx20Api: Stx20Api;

constructor(config: ConfigurationParameters) {
this.configuration = new Configuration(config);
Expand All @@ -44,5 +77,7 @@ export class StacksClient {
this.rosettaApi = new RosettaApi(this.configuration);
this.fungibleTokensApi = new FungibleTokensApi(this.configuration);
this.nonFungibleTokensApi = new NonFungibleTokensApi(this.configuration);
this.tokensApi = new TokensApi({ basePath: config.basePath });
this.stx20Api = new Stx20Api();
}
}
20 changes: 20 additions & 0 deletions src/app/query/stacks/stx20/stx20-tokens.hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import BigNumber from 'bignumber.js';

import { createMoney } from '@shared/models/money.model';

import type { Stx20Balance, Stx20Token } from '../stacks-client';
import { useStx20BalancesQuery } from './stx20-tokens.query';

function makeStx20Token(token: Stx20Balance): Stx20Token {
return {
balance: createMoney(new BigNumber(token.balance), token.ticker, 0),
marketData: null,
tokenData: token,
};
}

export function useStx20Tokens(address: string) {
return useStx20BalancesQuery(address, {
select: resp => resp.map(balance => makeStx20Token(balance)),
});
}
Loading

0 comments on commit e23c1a1

Please sign in to comment.