sui-explorer-local
Version:
Local Sui Explorer
121 lines (100 loc) • 3.36 kB
text/typescript
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
import { useSuiClient } from '@mysten/dapp-kit';
import { CoinMetadata } from '@mysten/sui.js/client';
import { SUI_TYPE_ARG } from '@mysten/sui.js/utils';
import { useQuery, type UseQueryResult } from '@tanstack/react-query';
import BigNumber from 'bignumber.js';
import { useMemo } from 'react';
import { formatAmount } from '../utils/formatAmount';
type FormattedCoin = [
formattedBalance: string,
coinSymbol: string,
queryResult: UseQueryResult<CoinMetadata | null>,
];
export enum CoinFormat {
ROUNDED = 'ROUNDED',
FULL = 'FULL',
}
/**
* Formats a coin balance based on our standard coin display logic.
* If the balance is less than 1, it will be displayed in its full decimal form.
* For values greater than 1, it will be truncated to 3 decimal places.
*/
export function formatBalance(
balance: bigint | number | string,
decimals: number,
format: CoinFormat = CoinFormat.ROUNDED,
) {
const bn = new BigNumber(balance.toString()).shiftedBy(-1 * decimals);
if (format === CoinFormat.FULL) {
return bn.toFormat();
}
return formatAmount(bn);
}
const ELLIPSIS = '\u{2026}';
const SYMBOL_TRUNCATE_LENGTH = 5;
const NAME_TRUNCATE_LENGTH = 10;
export function useCoinMetadata(coinType?: string | null) {
const client = useSuiClient();
return useQuery({
queryKey: ['coin-metadata', coinType],
queryFn: async () => {
if (!coinType) {
throw new Error('Fetching coin metadata should be disabled when coin type is disabled.');
}
// Optimize the known case of SUI to avoid a network call:
if (coinType === SUI_TYPE_ARG) {
const metadata: CoinMetadata = {
id: null,
decimals: 9,
description: '',
iconUrl: null,
name: 'Sui',
symbol: 'SUI',
};
return metadata;
}
return client.getCoinMetadata({ coinType });
},
select(data) {
if (!data) return null;
return {
...data,
symbol:
data.symbol.length > SYMBOL_TRUNCATE_LENGTH
? data.symbol.slice(0, SYMBOL_TRUNCATE_LENGTH) + ELLIPSIS
: data.symbol,
name:
data.name.length > NAME_TRUNCATE_LENGTH
? data.name.slice(0, NAME_TRUNCATE_LENGTH) + ELLIPSIS
: data.name,
};
},
retry: false,
enabled: !!coinType,
staleTime: Infinity,
gcTime: 24 * 60 * 60 * 1000,
});
}
// TODO #1: This handles undefined values to make it easier to integrate with
// the reset of the app as it is today, but it really shouldn't in a perfect world.
export function useFormatCoin(
balance?: bigint | number | string | null,
coinType?: string | null,
format: CoinFormat = CoinFormat.ROUNDED,
): FormattedCoin {
const fallbackSymbol = useMemo(() => (coinType ? getCoinSymbol(coinType) ?? '' : ''), [coinType]);
const queryResult = useCoinMetadata(coinType);
const { isFetched, data } = queryResult;
const formatted = useMemo(() => {
if (typeof balance === 'undefined' || balance === null) return '';
if (!isFetched) return '...';
return formatBalance(balance, data?.decimals ?? 0, format);
}, [data?.decimals, isFetched, balance, format]);
return [formatted, isFetched ? data?.symbol || fallbackSymbol : '', queryResult];
}
/** @deprecated use coin metadata instead */
export function getCoinSymbol(coinTypeArg: string) {
return coinTypeArg.substring(coinTypeArg.lastIndexOf(':') + 1);
}