Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: runes balances #5224

Merged
merged 1 commit into from
Apr 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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
20 changes: 14 additions & 6 deletions 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 @@ -23,11 +24,13 @@ import { StacksFungibleTokenAssetList } from './components/stacks-fungible-token

export function AssetsList() {
const hasBitcoinLedgerKeys = useHasBitcoinLedgerKeychain();
const btcAddress = useCurrentAccountNativeSegwitAddressIndexZero();
const bitcoinAddressNativeSegwit = useCurrentAccountNativeSegwitAddressIndexZero();
const { address: bitcoinAddressTaproot } = useCurrentAccountTaprootIndexZeroSigner();
const network = useCurrentNetwork();

const { btcAvailableAssetBalance, btcAvailableUsdBalance, isInitialLoading } =
useBtcAssetBalance(btcAddress);
const { btcAvailableAssetBalance, btcAvailableUsdBalance, isInitialLoading } = useBtcAssetBalance(
bitcoinAddressNativeSegwit
);

const { whenWallet } = useWalletType();

Expand All @@ -39,7 +42,7 @@ export function AssetsList() {
assetBalance={btcAvailableAssetBalance}
usdBalance={btcAvailableUsdBalance}
icon={<BtcAvatarIcon />}
address={btcAddress}
address={bitcoinAddressNativeSegwit}
isLoading={isInitialLoading}
/>
),
Expand All @@ -48,7 +51,7 @@ export function AssetsList() {
assetBalance={btcAvailableAssetBalance}
usdBalance={btcAvailableUsdBalance}
icon={<BtcAvatarIcon />}
address={btcAddress}
address={bitcoinAddressNativeSegwit}
isLoading={isInitialLoading}
rightElement={
hasBitcoinLedgerKeys ? undefined : <ConnectLedgerAssetBtn chain="bitcoin" />
Expand All @@ -74,7 +77,12 @@ export function AssetsList() {
</CurrentStacksAccountLoader>

{whenWallet({
software: <BitcoinFungibleTokenAssetList btcAddress={btcAddress} />,
software: (
<BitcoinFungibleTokenAssetList
btcAddressNativeSegwit={bitcoinAddressNativeSegwit}
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>
);
}
Loading