Skip to content

Commit

Permalink
feat: rune balances
Browse files Browse the repository at this point in the history
  • Loading branch information
fbwoolf committed Apr 12, 2024
1 parent 5aa7a20 commit 845fa88
Show file tree
Hide file tree
Showing 15 changed files with 174 additions and 21 deletions.
3 changes: 2 additions & 1 deletion config/wallet-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,5 +100,6 @@
"mainnetApiUrl": "https://api2.ordinalsbot.com",
"signetApiUrl": "https://signet.ordinalsbot.com"
},
"recoverUninscribedTaprootUtxosFeatureEnabled": false
"recoverUninscribedTaprootUtxosFeatureEnabled": false,
"runesEnabled": false
}
4 changes: 4 additions & 0 deletions config/wallet-config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,10 @@
"recoverUninscribedTaprootUtxosFeatureEnabled": {
"type": "boolean",
"description": "Determines whether or not the recover uninscribed taproot utxos feature is enabled"
},
"runesEnabled": {
"type": "boolean",
"description": "Determines whether or not Runes are live on mainnet"
}
},
"$defs": {
Expand Down
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 { RuneToken } from '@app/query/bitcoin/bitcoin-client';
import { RunesAvatarIcon } from '@app/ui/components/avatar/runes-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 RunesAssetItemLayoutProps {
rune: RuneToken;
}
export function RunesAssetItemLayout({ rune }: RunesAssetItemLayoutProps) {
const balance = rune.balance.amount.toString();
const formattedBalance = formatBalance(balance);

return (
<Pressable my="space.02">
<ItemLayout
flagImg={<RunesAvatarIcon />}
titleLeft={rune.rune_name.toUpperCase()}
captionLeft="RUNE"
titleRight={
<BasicTooltip
asChild
label={formattedBalance.isAbbreviated ? balance : undefined}
side="left"
>
<styled.span data-testid={rune.rune_name} fontWeight={500} textStyle="label.02">
{formattedBalance.value}
</styled.span>
</BasicTooltip>
}
/>
</Pressable>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { RuneToken } from '@app/query/bitcoin/bitcoin-client';

import { RunesAssetItemLayout } from './runes-asset-item.layout';

interface RunesAssetListProps {
runes: RuneToken[];
}
export function RunesAssetList({ runes }: RunesAssetListProps) {
return runes.map(rune => <RunesAssetItemLayout key={rune.rune_id} rune={rune} />);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { StacksFungibleTokenAsset } from '@shared/models/crypto-asset.model';
import { useWalletType } from '@app/common/use-wallet-type';
import { BitcoinNativeSegwitAccountLoader } from '@app/components/account/bitcoin-account-loader';
import { BitcoinBalanceLoader } from '@app/components/balance/bitcoin-balance-loader';
import { Brc20TokensLoader } from '@app/components/brc20-tokens-loader';
import { Brc20TokenAssetList } from '@app/components/crypto-assets/bitcoin/brc20-token-asset-list/brc20-token-asset-list';
import { Brc20TokensLoader } from '@app/components/loaders/brc20-tokens-loader';
import { BtcAvatarIcon } from '@app/ui/components/avatar/btc-avatar-icon';

import { CryptoCurrencyAssetItemLayout } from '../crypto-currency-asset/crypto-currency-asset-item.layout';
Expand Down
File renamed without changes.
11 changes: 11 additions & 0 deletions src/app/components/loaders/runes-loader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { RuneToken } from '@app/query/bitcoin/bitcoin-client';
import { useRuneTokens } from '@app/query/bitcoin/runes/runes.hooks';

interface RunesLoaderProps {
address: string;
children(runes: RuneToken[]): React.ReactNode;
}
export function RunesLoader({ address, children }: RunesLoaderProps) {
const { data: runes = [] } = useRuneTokens(address);
return children(runes);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ interface Src20TokensLoaderProps {
address: string;
children(src20Tokens: Src20Token[]): React.ReactNode;
}

export function Src20TokensLoader({ address, children }: Src20TokensLoaderProps) {
const { data: src20Tokens = [] } = useSrc20TokensByAddress(address);
return children(src20Tokens);
Expand Down
9 changes: 8 additions & 1 deletion src/app/features/asset-list/asset-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { CryptoCurrencyAssetItemLayout } from '@app/components/crypto-assets/cry
import { CurrentStacksAccountLoader } from '@app/components/loaders/stacks-account-loader';
import { useHasBitcoinLedgerKeychain } from '@app/store/accounts/blockchain/bitcoin/bitcoin.ledger';
import { useCurrentAccountNativeSegwitAddressIndexZero } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import { useCurrentAccountTaprootIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks';
import { useCurrentNetwork } from '@app/store/networks/networks.selectors';
import { BtcAvatarIcon } from '@app/ui/components/avatar/btc-avatar-icon';

Expand All @@ -24,6 +25,7 @@ import { StacksFungibleTokenAssetList } from './components/stacks-fungible-token
export function AssetsList() {
const hasBitcoinLedgerKeys = useHasBitcoinLedgerKeychain();
const btcAddress = useCurrentAccountNativeSegwitAddressIndexZero();
const { address: bitcoinAddressTaproot } = useCurrentAccountTaprootIndexZeroSigner();
const network = useCurrentNetwork();

const { btcAvailableAssetBalance, btcAvailableUsdBalance, isInitialLoading } =
Expand Down Expand Up @@ -74,7 +76,12 @@ export function AssetsList() {
</CurrentStacksAccountLoader>

{whenWallet({
software: <BitcoinFungibleTokenAssetList btcAddress={btcAddress} />,
software: (
<BitcoinFungibleTokenAssetList
btcAddressNativeSegwit={btcAddress}
btcAddressTaproot={bitcoinAddressTaproot}
/>
),
ledger: null,
})}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,29 @@
import { Brc20TokensLoader } from '@app/components/brc20-tokens-loader';
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 { Src20TokensLoader } from '@app/components/src20-tokens-loader';
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';

interface BitcoinFungibleTokenAssetListProps {
btcAddress: string;
btcAddressNativeSegwit: string;
btcAddressTaproot: string;
}
export function BitcoinFungibleTokenAssetList({ btcAddress }: BitcoinFungibleTokenAssetListProps) {
export function BitcoinFungibleTokenAssetList({
btcAddressNativeSegwit,
btcAddressTaproot,
}: BitcoinFungibleTokenAssetListProps) {
return (
<>
<Brc20TokensLoader>
{brc20Tokens => <Brc20TokenAssetList brc20Tokens={brc20Tokens} />}
</Brc20TokensLoader>
<Src20TokensLoader address={btcAddress}>
<Src20TokensLoader address={btcAddressNativeSegwit}>
{src20Tokens => <Src20TokenAssetList src20Tokens={src20Tokens} />}
</Src20TokensLoader>
<RunesLoader address={btcAddressTaproot}>
{runes => <RunesAssetList runes={runes} />}
</RunesLoader>
</>
);
}
28 changes: 20 additions & 8 deletions src/app/query/bitcoin/bitcoin-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import PQueue from 'p-queue';
import {
BESTINSLOT_API_BASE_URL_MAINNET,
BESTINSLOT_API_BASE_URL_TESTNET,
type BitcoinNetworkModes,
HIRO_API_BASE_URL_MAINNET,
} from '@shared/constants';
import { Paginated } from '@shared/models/api-types';
import type { Money } from '@shared/models/money.model';
import type { BitcoinTx } from '@shared/models/transactions/bitcoin-transaction.model';

import { getBlockstreamRatelimiter } from './blockstream-rate-limiter';
Expand Down Expand Up @@ -100,13 +102,22 @@ interface BestinslotBrc20AddressBalanceResponse {
data: Brc20TokenResponse[];
}

interface RunesWalletBalanceResponse {
export interface RuneBalance {
pkscript: string;
wallet_addr: string;
rune_id: string;
total_balance: string;
rune_name: string;
spaced_rune_name: string;
total_balance: string;
wallet_addr: string;
}

interface RunesWalletBalancesResponse {
block_height: number;
data: RuneBalance[];
}

export interface RuneToken extends RuneBalance {
balance: Money;
}

class BestinslotApi {
Expand Down Expand Up @@ -161,13 +172,14 @@ class BestinslotApi {
return resp.data;
}

/* RUNES ON TESTNET */
async getRunesWalletBalances(address: string) {
const resp = await axios.get<RunesWalletBalanceResponse[]>(
`${this.testnetUrl}/runes/wallet_balances?address=${address}`,
/* RUNES */
async getRunesWalletBalances(address: string, network: BitcoinNetworkModes) {
const baseUrl = network === 'mainnet' ? this.url : this.testnetUrl;
const resp = await axios.get<RunesWalletBalancesResponse>(
`${baseUrl}/runes/wallet_balances?address=${address}`,
{ ...this.defaultOptions }
);
return resp.data;
return resp.data.data;
}
}

Expand Down
17 changes: 13 additions & 4 deletions src/app/query/bitcoin/runes/runes-wallet-balances.query.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
import { useQuery } from '@tanstack/react-query';

import { useConfigRunesEnabled } from '@app/query/common/remote-config/remote-config.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';

// ts-unused-exports:disable-next-line
export function useGetRunesWalletBalancesQuery(address: string) {
import type { RuneBalance } from '../bitcoin-client';

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

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

import type { RuneToken } from '../bitcoin-client';
import { useGetRunesWalletBalancesQuery } from './runes-wallet-balances.query';

export function useRuneTokens(address: string) {
return useGetRunesWalletBalancesQuery(address, {
select: resp =>
resp.map(rune => {
return {
...rune,
balance: createMoney(Number(rune.total_balance), rune.rune_name, 0),
} as RuneToken;
}),
});
}
5 changes: 5 additions & 0 deletions src/app/query/common/remote-config/remote-config.query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,3 +173,8 @@ export function useConfigOrdinalsbot() {
signetApiUrl: get(config, 'ordinalsbot.signetApiUrl', 'https://signet.ordinalsbot.com'),
};
}

export function useConfigRunesEnabled() {
const config = useRemoteConfig();
return get(config, 'runesEnabled', false);
}
33 changes: 33 additions & 0 deletions src/app/ui/components/avatar/runes-avatar-icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Avatar, type AvatarProps } from './avatar';

export function RunesAvatarIcon(props: AvatarProps) {
return (
<Avatar.Root {...props}>
<Avatar.Svg>
<circle cx="16" cy="16" r="16" fill="black" />
<circle cx="16" cy="16" r="15.5" stroke="#B1977B" strokeOpacity="0.1" />
<g clipPath="url(#clip0_15326_75304)">
<rect width="18" height="17.1" transform="translate(7 7.94995)" fill="black" />
<path d="M8.61722 5.97876L23.3818 26.8953" stroke="white" strokeWidth="2.25" />
<path d="M23.3823 5.97876L8.61768 26.8953" stroke="white" strokeWidth="2.25" />
<path d="M15.9998 7.82446L15.9998 25.0499" stroke="white" strokeWidth="1.75" />
<path
d="M20.0137 10.7512L22.8552 16.4273L20.0656 22.1859"
stroke="white"
strokeWidth="1.75"
/>
<path
d="M11.999 10.7512L9.15748 16.4273L11.9471 22.1859"
stroke="white"
strokeWidth="1.75"
/>
</g>
<defs>
<clipPath id="clip0_15326_75304">
<rect width="18" height="17.1" fill="white" transform="translate(7 7.94995)" />
</clipPath>
</defs>
</Avatar.Svg>
</Avatar.Root>
);
}

0 comments on commit 845fa88

Please sign in to comment.