From 9700f853b84c44ee1f818c259fccbc97d6c13092 Mon Sep 17 00:00:00 2001 From: Fara Woolf Date: Mon, 25 Mar 2024 20:07:22 -0500 Subject: [PATCH] feat: add src-20 token balances, closes #3751 --- src/app/common/hooks/use-brc20-tokens.ts | 2 +- src/app/components/brc20-tokens-loader.tsx | 5 +-- .../brc20-token-asset-item.layout.tsx | 12 ++---- .../brc20-token-asset-list.tsx | 17 ++++---- .../src20-token-asset-item.layout.tsx | 39 +++++++++++++++++++ .../src20-token-asset-list.tsx | 10 +++++ .../choose-crypto-asset/crypto-asset-list.tsx | 2 +- src/app/components/src20-tokens-loader.tsx | 12 ++++++ src/app/features/asset-list/asset-list.tsx | 7 +--- .../bitcoin-fungible-tokens-asset-list.tsx | 27 ++++++------- .../components/bitcoin/stamps.tsx | 4 +- .../bitcoin/stamps/stamps-by-address.hooks.ts | 17 ++++++++ .../bitcoin/stamps/stamps-by-address.query.ts | 8 ++-- 13 files changed, 115 insertions(+), 47 deletions(-) rename src/app/components/crypto-assets/bitcoin/brc20-token-asset-list/{components => }/brc20-token-asset-item.layout.tsx (80%) create mode 100644 src/app/components/crypto-assets/bitcoin/src20-token-asset-list/src20-token-asset-item.layout.tsx create mode 100644 src/app/components/crypto-assets/bitcoin/src20-token-asset-list/src20-token-asset-list.tsx create mode 100644 src/app/components/src20-tokens-loader.tsx create mode 100644 src/app/query/bitcoin/stamps/stamps-by-address.hooks.ts diff --git a/src/app/common/hooks/use-brc20-tokens.ts b/src/app/common/hooks/use-brc20-tokens.ts index 9e638d67e31..b11a1c19a35 100644 --- a/src/app/common/hooks/use-brc20-tokens.ts +++ b/src/app/common/hooks/use-brc20-tokens.ts @@ -7,5 +7,5 @@ export function useBrc20Tokens() { .filter(token => token.length > 0) .flatMap(token => token); - return brc20Tokens; + return brc20Tokens ?? []; } diff --git a/src/app/components/brc20-tokens-loader.tsx b/src/app/components/brc20-tokens-loader.tsx index 5ef4848e4b3..ab57777495d 100644 --- a/src/app/components/brc20-tokens-loader.tsx +++ b/src/app/components/brc20-tokens-loader.tsx @@ -2,12 +2,9 @@ import { useBrc20Tokens } from '@app/common/hooks/use-brc20-tokens'; import { Brc20Token } from '@app/query/bitcoin/bitcoin-client'; interface Brc20TokensLoaderProps { - children(brc20Tokens: Brc20Token[]): React.JSX.Element; + children(brc20Tokens: Brc20Token[]): React.ReactNode; } - export function Brc20TokensLoader({ children }: Brc20TokensLoaderProps) { const brc20Tokens = useBrc20Tokens(); - - if (!brc20Tokens) return null; return children(brc20Tokens); } diff --git a/src/app/components/crypto-assets/bitcoin/brc20-token-asset-list/components/brc20-token-asset-item.layout.tsx b/src/app/components/crypto-assets/bitcoin/brc20-token-asset-list/brc20-token-asset-item.layout.tsx similarity index 80% rename from src/app/components/crypto-assets/bitcoin/brc20-token-asset-list/components/brc20-token-asset-item.layout.tsx rename to src/app/components/crypto-assets/bitcoin/brc20-token-asset-list/brc20-token-asset-item.layout.tsx index 620d4c768ed..84924791aaa 100644 --- a/src/app/components/crypto-assets/bitcoin/brc20-token-asset-list/components/brc20-token-asset-item.layout.tsx +++ b/src/app/components/crypto-assets/bitcoin/brc20-token-asset-list/brc20-token-asset-item.layout.tsx @@ -13,13 +13,9 @@ interface Brc20TokenAssetItemLayoutProps { token: Brc20Token; onClick?(): void; } -export function Brc20TokenAssetItemLayout({ - onClick, - - token, -}: Brc20TokenAssetItemLayoutProps) { - const balance = createMoney(Number(token.overall_balance), token.ticker, 0); - const formattedBalance = formatBalance(balance.amount.toString()); +export function Brc20TokenAssetItemLayout({ onClick, token }: Brc20TokenAssetItemLayoutProps) { + const balance = createMoney(Number(token.overall_balance), token.ticker, 0).amount.toString(); + const formattedBalance = formatBalance(balance); return ( @@ -30,7 +26,7 @@ export function Brc20TokenAssetItemLayout({ titleRight={ diff --git a/src/app/components/crypto-assets/bitcoin/brc20-token-asset-list/brc20-token-asset-list.tsx b/src/app/components/crypto-assets/bitcoin/brc20-token-asset-list/brc20-token-asset-list.tsx index 2e405a07c90..066739c9966 100644 --- a/src/app/components/crypto-assets/bitcoin/brc20-token-asset-list/brc20-token-asset-list.tsx +++ b/src/app/components/crypto-assets/bitcoin/brc20-token-asset-list/brc20-token-asset-list.tsx @@ -4,22 +4,25 @@ import { CryptoAssetSelectors } from '@tests/selectors/crypto-asset.selectors'; import { Stack } from 'leather-styles/jsx'; import { RouteUrls } from '@shared/route-urls'; -import { noop } from '@shared/utils'; import { useNativeSegwitBalance } from '@app/query/bitcoin/balance/btc-native-segwit-balance.hooks'; import { Brc20Token } from '@app/query/bitcoin/bitcoin-client'; import { useCurrentAccountNativeSegwitAddressIndexZero } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; -import { Brc20TokenAssetItemLayout } from './components/brc20-token-asset-item.layout'; +import { Brc20TokenAssetItemLayout } from './brc20-token-asset-item.layout'; -export function Brc20TokenAssetList(props: { brc20Tokens?: Brc20Token[] }) { +interface Brc20TokenAssetListProps { + brc20Tokens: Brc20Token[]; + variant?: string; +} +export function Brc20TokenAssetList({ brc20Tokens, variant }: Brc20TokenAssetListProps) { const navigate = useNavigate(); const currentAccountBtcAddress = useCurrentAccountNativeSegwitAddressIndexZero(); const { btcBalance: btcCryptoCurrencyAssetBalance } = useNativeSegwitBalance(currentAccountBtcAddress); const hasPositiveBtcBalanceForFees = - btcCryptoCurrencyAssetBalance.balance.amount.isGreaterThan(0); + variant === 'send' && btcCryptoCurrencyAssetBalance.balance.amount.isGreaterThan(0); function navigateToBrc20SendForm(token: Brc20Token) { const { ticker, available_balance, decimals, holderAddress } = token; @@ -28,15 +31,13 @@ export function Brc20TokenAssetList(props: { brc20Tokens?: Brc20Token[] }) { }); } - if (!props.brc20Tokens?.length) return null; - return ( - {props.brc20Tokens?.map(token => ( + {brc20Tokens.map(token => ( navigateToBrc20SendForm(token) : noop} + onClick={hasPositiveBtcBalanceForFees ? () => navigateToBrc20SendForm(token) : undefined} /> ))} diff --git a/src/app/components/crypto-assets/bitcoin/src20-token-asset-list/src20-token-asset-item.layout.tsx b/src/app/components/crypto-assets/bitcoin/src20-token-asset-list/src20-token-asset-item.layout.tsx new file mode 100644 index 00000000000..e2e669f1e92 --- /dev/null +++ b/src/app/components/crypto-assets/bitcoin/src20-token-asset-list/src20-token-asset-item.layout.tsx @@ -0,0 +1,39 @@ +import { styled } from 'leather-styles/jsx'; + +import { createMoney } from '@shared/models/money.model'; + +import { formatBalance } from '@app/common/format-balance'; +import type { Src20Token } from '@app/query/bitcoin/stamps/stamps-by-address.query'; +import { Src20AvatarIcon } from '@app/ui/components/avatar/src20-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 Src20TokenAssetItemLayoutProps { + token: Src20Token; +} +export function Src20TokenAssetItemLayout({ token }: Src20TokenAssetItemLayoutProps) { + const balance = createMoney(Number(token.amt), token.tick, 0).amount.toString(); + const formattedBalance = formatBalance(balance); + + return ( + + } + titleLeft={token.tick.toUpperCase()} + captionLeft="SRC-20" + titleRight={ + + + {formattedBalance.value} + + + } + /> + + ); +} diff --git a/src/app/components/crypto-assets/bitcoin/src20-token-asset-list/src20-token-asset-list.tsx b/src/app/components/crypto-assets/bitcoin/src20-token-asset-list/src20-token-asset-list.tsx new file mode 100644 index 00000000000..04a8983a483 --- /dev/null +++ b/src/app/components/crypto-assets/bitcoin/src20-token-asset-list/src20-token-asset-list.tsx @@ -0,0 +1,10 @@ +import type { Src20Token } from '@app/query/bitcoin/stamps/stamps-by-address.query'; + +import { Src20TokenAssetItemLayout } from './src20-token-asset-item.layout'; + +interface Src20TokenAssetListProps { + src20Tokens: Src20Token[]; +} +export function Src20TokenAssetList({ src20Tokens }: Src20TokenAssetListProps) { + return src20Tokens.map(token => ); +} 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 13de5d43f0a..a3e43964c69 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 @@ -58,7 +58,7 @@ export function CryptoAssetList({ {() => ( - {brc20Tokens => } + {brc20Tokens => } )} diff --git a/src/app/components/src20-tokens-loader.tsx b/src/app/components/src20-tokens-loader.tsx new file mode 100644 index 00000000000..516dcb13172 --- /dev/null +++ b/src/app/components/src20-tokens-loader.tsx @@ -0,0 +1,12 @@ +import { useSrc20TokensByAddress } from '@app/query/bitcoin/stamps/stamps-by-address.hooks'; +import type { Src20Token } from '@app/query/bitcoin/stamps/stamps-by-address.query'; + +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 638d710153f..ed1ddd894bf 100644 --- a/src/app/features/asset-list/asset-list.tsx +++ b/src/app/features/asset-list/asset-list.tsx @@ -6,7 +6,6 @@ import { Stack } from 'leather-styles/jsx'; import { useBtcAssetBalance } from '@app/common/hooks/balance/btc/use-btc-balance'; import { useWalletType } from '@app/common/use-wallet-type'; import { BitcoinContractEntryPoint } from '@app/components/bitcoin-contract-entry-point/bitcoin-contract-entry-point'; -import { Brc20TokensLoader } from '@app/components/brc20-tokens-loader'; import { CryptoCurrencyAssetItemLayout } from '@app/components/crypto-assets/crypto-currency-asset/crypto-currency-asset-item.layout'; import { CurrentStacksAccountLoader } from '@app/components/loaders/stacks-account-loader'; import { useHasBitcoinLedgerKeychain } from '@app/store/accounts/blockchain/bitcoin/bitcoin.ledger'; @@ -75,11 +74,7 @@ export function AssetsList() { {whenWallet({ - software: ( - - {brc20Tokens => } - - ), + 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 2298191b065..1826fa6b72a 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,19 +1,20 @@ -import { Stack } from 'leather-styles/jsx'; - -import { Brc20TokenAssetItemLayout } from '@app/components/crypto-assets/bitcoin/brc20-token-asset-list/components/brc20-token-asset-item.layout'; -import { Brc20Token } from '@app/query/bitcoin/bitcoin-client'; +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 { Src20TokenAssetList } from '@app/components/crypto-assets/bitcoin/src20-token-asset-list/src20-token-asset-list'; +import { Src20TokensLoader } from '@app/components/src20-tokens-loader'; interface BitcoinFungibleTokenAssetListProps { - brc20Tokens?: Brc20Token[]; + btcAddress: string; } -export function BitcoinFungibleTokenAssetList({ brc20Tokens }: BitcoinFungibleTokenAssetListProps) { - if (!brc20Tokens) return null; - +export function BitcoinFungibleTokenAssetList({ btcAddress }: BitcoinFungibleTokenAssetListProps) { return ( - - {brc20Tokens.map(token => ( - - ))} - + <> + + {brc20Tokens => } + + + {src20Tokens => } + + ); } diff --git a/src/app/features/collectibles/components/bitcoin/stamps.tsx b/src/app/features/collectibles/components/bitcoin/stamps.tsx index b3c7c8bd639..542e6953392 100644 --- a/src/app/features/collectibles/components/bitcoin/stamps.tsx +++ b/src/app/features/collectibles/components/bitcoin/stamps.tsx @@ -1,14 +1,14 @@ import { useEffect } from 'react'; import { useAnalytics } from '@app/common/hooks/analytics/use-analytics'; -import { useStampsByAddressQuery } from '@app/query/bitcoin/stamps/stamps-by-address.query'; +import { useStampsByAddress } from '@app/query/bitcoin/stamps/stamps-by-address.hooks'; import { useCurrentAccountNativeSegwitAddressIndexZero } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; import { Stamp } from './stamp'; export function Stamps() { const currentAccountBtcAddress = useCurrentAccountNativeSegwitAddressIndexZero(); - const { data: stamps = [] } = useStampsByAddressQuery(currentAccountBtcAddress); + const { data: stamps = [] } = useStampsByAddress(currentAccountBtcAddress); const analytics = useAnalytics(); useEffect(() => { diff --git a/src/app/query/bitcoin/stamps/stamps-by-address.hooks.ts b/src/app/query/bitcoin/stamps/stamps-by-address.hooks.ts new file mode 100644 index 00000000000..5e958187b93 --- /dev/null +++ b/src/app/query/bitcoin/stamps/stamps-by-address.hooks.ts @@ -0,0 +1,17 @@ +import { useStampsByAddressQuery } from './stamps-by-address.query'; + +export function useStampsByAddress(address: string) { + return useStampsByAddressQuery(address, { + select(data) { + return data.data.stamps; + }, + }); +} + +export function useSrc20TokensByAddress(address: string) { + return useStampsByAddressQuery(address, { + select(data) { + return data.data.src20; + }, + }); +} diff --git a/src/app/query/bitcoin/stamps/stamps-by-address.query.ts b/src/app/query/bitcoin/stamps/stamps-by-address.query.ts index 9080c666724..f5d109f91c9 100644 --- a/src/app/query/bitcoin/stamps/stamps-by-address.query.ts +++ b/src/app/query/bitcoin/stamps/stamps-by-address.query.ts @@ -31,7 +31,7 @@ export interface Stamp { file_hash: string; } -interface Src20 { +export interface Src20Token { id: string; address: string; cpid: string; @@ -57,18 +57,18 @@ interface StampsByAddressQueryResponse { }; data: { stamps: Stamp[]; - src20: Src20[]; + src20: Src20Token[]; }; } /** * @see https://stampchain.io/docs#/default/get_api_v2_balance__address_ */ -async function fetchStampsByAddress(address: string): Promise { +async function fetchStampsByAddress(address: string): Promise { const resp = await axios.get( `https://stampchain.io/api/v2/balance/${address}` ); - return resp.data.data.stamps; + return resp.data; } type FetchStampsByAddressResp = Awaited>;