Skip to content

Commit

Permalink
feat: add src-20 token balances, closes #3751
Browse files Browse the repository at this point in the history
  • Loading branch information
fbwoolf committed Apr 4, 2024
1 parent 20418ab commit 9700f85
Show file tree
Hide file tree
Showing 13 changed files with 115 additions and 47 deletions.
2 changes: 1 addition & 1 deletion src/app/common/hooks/use-brc20-tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ export function useBrc20Tokens() {
.filter(token => token.length > 0)
.flatMap(token => token);

return brc20Tokens;
return brc20Tokens ?? [];
}
5 changes: 1 addition & 4 deletions src/app/components/brc20-tokens-loader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Pressable onClick={onClick} my="space.02">
Expand All @@ -30,7 +26,7 @@ export function Brc20TokenAssetItemLayout({
titleRight={
<BasicTooltip
asChild
label={formattedBalance.isAbbreviated ? balance.amount.toString() : undefined}
label={formattedBalance.isAbbreviated ? balance : undefined}
side="left"
>
<styled.span data-testid={token.ticker} textStyle="label.02">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -28,15 +31,13 @@ export function Brc20TokenAssetList(props: { brc20Tokens?: Brc20Token[] }) {
});
}

if (!props.brc20Tokens?.length) return null;

return (
<Stack data-testid={CryptoAssetSelectors.CryptoAssetList}>
{props.brc20Tokens?.map(token => (
{brc20Tokens.map(token => (
<Brc20TokenAssetItemLayout
key={token.ticker}
token={token}
onClick={hasPositiveBtcBalanceForFees ? () => navigateToBrc20SendForm(token) : noop}
onClick={hasPositiveBtcBalanceForFees ? () => navigateToBrc20SendForm(token) : undefined}
/>
))}
</Stack>
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<Pressable my="space.02">
<ItemLayout
flagImg={<Src20AvatarIcon />}
titleLeft={token.tick.toUpperCase()}
captionLeft="SRC-20"
titleRight={
<BasicTooltip
asChild
label={formattedBalance.isAbbreviated ? balance : undefined}
side="left"
>
<styled.span data-testid={token.tick} 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 { 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 => <Src20TokenAssetItemLayout key={token.id} token={token} />);
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export function CryptoAssetList({
<BitcoinNativeSegwitAccountLoader current>
{() => (
<Brc20TokensLoader>
{brc20Tokens => <Brc20TokenAssetList brc20Tokens={brc20Tokens} />}
{brc20Tokens => <Brc20TokenAssetList brc20Tokens={brc20Tokens} variant="send" />}
</Brc20TokensLoader>
)}
</BitcoinNativeSegwitAccountLoader>
Expand Down
12 changes: 12 additions & 0 deletions src/app/components/src20-tokens-loader.tsx
Original file line number Diff line number Diff line change
@@ -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);
}
7 changes: 1 addition & 6 deletions src/app/features/asset-list/asset-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -75,11 +74,7 @@ export function AssetsList() {
</CurrentStacksAccountLoader>

{whenWallet({
software: (
<Brc20TokensLoader>
{brc20Tokens => <BitcoinFungibleTokenAssetList brc20Tokens={brc20Tokens} />}
</Brc20TokensLoader>
),
software: <BitcoinFungibleTokenAssetList btcAddress={btcAddress} />,
ledger: null,
})}

Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<Stack gap="space.05">
{brc20Tokens.map(token => (
<Brc20TokenAssetItemLayout key={token.ticker} token={token} />
))}
</Stack>
<>
<Brc20TokensLoader>
{brc20Tokens => <Brc20TokenAssetList brc20Tokens={brc20Tokens} />}
</Brc20TokensLoader>
<Src20TokensLoader address={btcAddress}>
{src20Tokens => <Src20TokenAssetList src20Tokens={src20Tokens} />}
</Src20TokensLoader>
</>
);
}
4 changes: 2 additions & 2 deletions src/app/features/collectibles/components/bitcoin/stamps.tsx
Original file line number Diff line number Diff line change
@@ -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(() => {
Expand Down
17 changes: 17 additions & 0 deletions src/app/query/bitcoin/stamps/stamps-by-address.hooks.ts
Original file line number Diff line number Diff line change
@@ -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;
},
});
}
8 changes: 4 additions & 4 deletions src/app/query/bitcoin/stamps/stamps-by-address.query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export interface Stamp {
file_hash: string;
}

interface Src20 {
export interface Src20Token {
id: string;
address: string;
cpid: string;
Expand All @@ -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<Stamp[]> {
async function fetchStampsByAddress(address: string): Promise<StampsByAddressQueryResponse> {
const resp = await axios.get<StampsByAddressQueryResponse>(
`https://stampchain.io/api/v2/balance/${address}`
);
return resp.data.data.stamps;
return resp.data;
}

type FetchStampsByAddressResp = Awaited<ReturnType<typeof fetchStampsByAddress>>;
Expand Down

0 comments on commit 9700f85

Please sign in to comment.