From 845fa884c72792c04680e016811be69cafce4c12 Mon Sep 17 00:00:00 2001 From: Fara Woolf Date: Thu, 11 Apr 2024 12:46:22 -0500 Subject: [PATCH] feat: rune balances --- config/wallet-config.json | 3 +- config/wallet-config.schema.json | 4 ++ .../runes-asset-item.layout.tsx | 37 +++++++++++++++++++ .../runes-asset-list/runes-asset-list.tsx | 10 +++++ .../choose-crypto-asset/crypto-asset-list.tsx | 2 +- .../{ => loaders}/brc20-tokens-loader.tsx | 0 src/app/components/loaders/runes-loader.tsx | 11 ++++++ .../{ => loaders}/src20-tokens-loader.tsx | 1 - src/app/features/asset-list/asset-list.tsx | 9 ++++- .../bitcoin-fungible-tokens-asset-list.tsx | 19 +++++++--- src/app/query/bitcoin/bitcoin-client.ts | 28 ++++++++++---- .../runes/runes-wallet-balances.query.ts | 17 +++++++-- src/app/query/bitcoin/runes/runes.hooks.ts | 16 ++++++++ .../remote-config/remote-config.query.ts | 5 +++ .../components/avatar/runes-avatar-icon.tsx | 33 +++++++++++++++++ 15 files changed, 174 insertions(+), 21 deletions(-) create mode 100644 src/app/components/crypto-assets/bitcoin/runes-asset-list/runes-asset-item.layout.tsx create mode 100644 src/app/components/crypto-assets/bitcoin/runes-asset-list/runes-asset-list.tsx rename src/app/components/{ => loaders}/brc20-tokens-loader.tsx (100%) create mode 100644 src/app/components/loaders/runes-loader.tsx rename src/app/components/{ => loaders}/src20-tokens-loader.tsx (99%) create mode 100644 src/app/query/bitcoin/runes/runes.hooks.ts create mode 100644 src/app/ui/components/avatar/runes-avatar-icon.tsx 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..68aef22f84b 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'; @@ -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 } = @@ -74,7 +76,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 ( + + + + + + + + + + + + + + + + + + + + ); +}