diff --git a/config/wallet-config.json b/config/wallet-config.json
index 5910eca4e49..117ec770cdb 100644
--- a/config/wallet-config.json
+++ b/config/wallet-config.json
@@ -100,5 +100,6 @@
"mainnetApiUrl": "https://api2.ordinalsbot.com",
"signetApiUrl": "https://signet.ordinalsbot.com"
},
- "recoverUninscribedTaprootUtxosFeatureEnabled": false
+ "recoverUninscribedTaprootUtxosFeatureEnabled": false,
+ "runesEnabled": false
}
diff --git a/config/wallet-config.schema.json b/config/wallet-config.schema.json
index 07f3a5ae28b..ee3c62560ef 100644
--- a/config/wallet-config.schema.json
+++ b/config/wallet-config.schema.json
@@ -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": {
diff --git a/src/app/components/crypto-assets/bitcoin/runes-asset-list/runes-asset-item.layout.tsx b/src/app/components/crypto-assets/bitcoin/runes-asset-list/runes-asset-item.layout.tsx
new file mode 100644
index 00000000000..01501396b3d
--- /dev/null
+++ b/src/app/components/crypto-assets/bitcoin/runes-asset-list/runes-asset-item.layout.tsx
@@ -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 (
+
+ }
+ titleLeft={rune.rune_name.toUpperCase()}
+ captionLeft="RUNE"
+ titleRight={
+
+
+ {formattedBalance.value}
+
+
+ }
+ />
+
+ );
+}
diff --git a/src/app/components/crypto-assets/bitcoin/runes-asset-list/runes-asset-list.tsx b/src/app/components/crypto-assets/bitcoin/runes-asset-list/runes-asset-list.tsx
new file mode 100644
index 00000000000..a862ad36d8d
--- /dev/null
+++ b/src/app/components/crypto-assets/bitcoin/runes-asset-list/runes-asset-list.tsx
@@ -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 => );
+}
diff --git a/src/app/components/crypto-assets/choose-crypto-asset/crypto-asset-list.tsx b/src/app/components/crypto-assets/choose-crypto-asset/crypto-asset-list.tsx
index a3e43964c69..e1778a5d097 100644
--- a/src/app/components/crypto-assets/choose-crypto-asset/crypto-asset-list.tsx
+++ b/src/app/components/crypto-assets/choose-crypto-asset/crypto-asset-list.tsx
@@ -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';
diff --git a/src/app/components/brc20-tokens-loader.tsx b/src/app/components/loaders/brc20-tokens-loader.tsx
similarity index 100%
rename from src/app/components/brc20-tokens-loader.tsx
rename to src/app/components/loaders/brc20-tokens-loader.tsx
diff --git a/src/app/components/loaders/runes-loader.tsx b/src/app/components/loaders/runes-loader.tsx
new file mode 100644
index 00000000000..0d5e985fd6b
--- /dev/null
+++ b/src/app/components/loaders/runes-loader.tsx
@@ -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);
+}
diff --git a/src/app/components/src20-tokens-loader.tsx b/src/app/components/loaders/src20-tokens-loader.tsx
similarity index 99%
rename from src/app/components/src20-tokens-loader.tsx
rename to src/app/components/loaders/src20-tokens-loader.tsx
index 516dcb13172..170fdbfc3d1 100644
--- a/src/app/components/src20-tokens-loader.tsx
+++ b/src/app/components/loaders/src20-tokens-loader.tsx
@@ -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);
diff --git a/src/app/features/asset-list/asset-list.tsx b/src/app/features/asset-list/asset-list.tsx
index ed1ddd894bf..36c569e7976 100644
--- a/src/app/features/asset-list/asset-list.tsx
+++ b/src/app/features/asset-list/asset-list.tsx
@@ -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';
@@ -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();
@@ -39,7 +42,7 @@ export function AssetsList() {
assetBalance={btcAvailableAssetBalance}
usdBalance={btcAvailableUsdBalance}
icon={}
- address={btcAddress}
+ address={bitcoinAddressNativeSegwit}
isLoading={isInitialLoading}
/>
),
@@ -48,7 +51,7 @@ export function AssetsList() {
assetBalance={btcAvailableAssetBalance}
usdBalance={btcAvailableUsdBalance}
icon={}
- address={btcAddress}
+ address={bitcoinAddressNativeSegwit}
isLoading={isInitialLoading}
rightElement={
hasBitcoinLedgerKeys ? undefined :
@@ -74,7 +77,12 @@ export function AssetsList() {
{whenWallet({
- software: ,
+ software: (
+
+ ),
ledger: null,
})}
diff --git a/src/app/features/asset-list/components/bitcoin-fungible-tokens-asset-list.tsx b/src/app/features/asset-list/components/bitcoin-fungible-tokens-asset-list.tsx
index 1826fa6b72a..8f0edcc0b45 100644
--- a/src/app/features/asset-list/components/bitcoin-fungible-tokens-asset-list.tsx
+++ b/src/app/features/asset-list/components/bitcoin-fungible-tokens-asset-list.tsx
@@ -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 (
<>
{brc20Tokens => }
-
+
{src20Tokens => }
+
+ {runes => }
+
>
);
}
diff --git a/src/app/query/bitcoin/bitcoin-client.ts b/src/app/query/bitcoin/bitcoin-client.ts
index 870eeba5acd..5f86e5633bb 100644
--- a/src/app/query/bitcoin/bitcoin-client.ts
+++ b/src/app/query/bitcoin/bitcoin-client.ts
@@ -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';
@@ -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 {
@@ -161,13 +172,14 @@ class BestinslotApi {
return resp.data;
}
- /* RUNES ON TESTNET */
- async getRunesWalletBalances(address: string) {
- const resp = await axios.get(
- `${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(
+ `${baseUrl}/runes/wallet_balances?address=${address}`,
{ ...this.defaultOptions }
);
- return resp.data;
+ return resp.data.data;
}
}
diff --git a/src/app/query/bitcoin/runes/runes-wallet-balances.query.ts b/src/app/query/bitcoin/runes/runes-wallet-balances.query.ts
index a0e584d2506..f4837de55c3 100644
--- a/src/app/query/bitcoin/runes/runes-wallet-balances.query.ts
+++ b/src/app/query/bitcoin/runes/runes-wallet-balances.query.ts
@@ -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(
+ address: string,
+ options?: AppUseQueryConfig
+) {
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,
});
}
diff --git a/src/app/query/bitcoin/runes/runes.hooks.ts b/src/app/query/bitcoin/runes/runes.hooks.ts
new file mode 100644
index 00000000000..0d614b94607
--- /dev/null
+++ b/src/app/query/bitcoin/runes/runes.hooks.ts
@@ -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;
+ }),
+ });
+}
diff --git a/src/app/query/common/remote-config/remote-config.query.ts b/src/app/query/common/remote-config/remote-config.query.ts
index 657ce479a3a..81dfe4ee822 100644
--- a/src/app/query/common/remote-config/remote-config.query.ts
+++ b/src/app/query/common/remote-config/remote-config.query.ts
@@ -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);
+}
diff --git a/src/app/ui/components/avatar/runes-avatar-icon.tsx b/src/app/ui/components/avatar/runes-avatar-icon.tsx
new file mode 100644
index 00000000000..6902fabf053
--- /dev/null
+++ b/src/app/ui/components/avatar/runes-avatar-icon.tsx
@@ -0,0 +1,33 @@
+import { Avatar, type AvatarProps } from './avatar';
+
+export function RunesAvatarIcon(props: AvatarProps) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}