diff --git a/package-lock.json b/package-lock.json index 9ea2d4cdc..bbdf5a7a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,7 @@ "recharts": "^2.15.3", "ripple-address-codec": "^5.0.0", "ripple-binary-codec": "^2.9.0", + "tldts": "^6.1.86", "topojson-client": "^3.0.0", "usehooks-ts": "^3.1.0", "vite": "^7.3.2", diff --git a/package.json b/package.json index cc8d56238..5e4abf037 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "recharts": "^2.15.3", "ripple-address-codec": "^5.0.0", "ripple-binary-codec": "^2.9.0", + "tldts": "^6.1.86", "topojson-client": "^3.0.0", "usehooks-ts": "^3.1.0", "vite": "^7.3.2", diff --git a/public/locales/ca-CA/translations.json b/public/locales/ca-CA/translations.json index bc4553c11..cb84b3477 100644 --- a/public/locales/ca-CA/translations.json +++ b/public/locales/ca-CA/translations.json @@ -740,7 +740,6 @@ "account_page_asset_table_column_asset_class": null, "account_page_asset_table_column_balance": null, "account_page_asset_table_column_balance_usd": null, - "account_page_asset_table_column_circulating_supply": null, "account_page_asset_table_column_currency_code": null, "account_page_asset_table_column_frozen": null, "account_page_asset_table_column_highest_bid": null, diff --git a/public/locales/en-US/translations.json b/public/locales/en-US/translations.json index 8c55a6a7f..8bb5c5133 100644 --- a/public/locales/en-US/translations.json +++ b/public/locales/en-US/translations.json @@ -741,7 +741,6 @@ "account_page_asset_table_column_asset_class": "Asset Class", "account_page_asset_table_column_balance": "Balance", "account_page_asset_table_column_balance_usd": "Balance (USD)", - "account_page_asset_table_column_circulating_supply": "Circ Supply", "account_page_asset_table_column_confidential_balance": "Conf Balance", "account_page_asset_table_column_currency_code": "Currency Code", "account_page_asset_table_column_frozen": "Frozen", diff --git a/public/locales/es-ES/translations.json b/public/locales/es-ES/translations.json index 12e1a48f4..5e80854b4 100644 --- a/public/locales/es-ES/translations.json +++ b/public/locales/es-ES/translations.json @@ -741,7 +741,6 @@ "account_page_asset_table_column_asset_class": null, "account_page_asset_table_column_balance": null, "account_page_asset_table_column_balance_usd": null, - "account_page_asset_table_column_circulating_supply": null, "account_page_asset_table_column_currency_code": null, "account_page_asset_table_column_frozen": null, "account_page_asset_table_column_highest_bid": null, diff --git a/public/locales/fr-FR/translations.json b/public/locales/fr-FR/translations.json index 99bb71f9e..a2e67b4b5 100644 --- a/public/locales/fr-FR/translations.json +++ b/public/locales/fr-FR/translations.json @@ -741,7 +741,6 @@ "account_page_asset_table_column_asset_class": null, "account_page_asset_table_column_balance": null, "account_page_asset_table_column_balance_usd": null, - "account_page_asset_table_column_circulating_supply": null, "account_page_asset_table_column_currency_code": null, "account_page_asset_table_column_frozen": null, "account_page_asset_table_column_highest_bid": null, diff --git a/public/locales/ja-JP/translations.json b/public/locales/ja-JP/translations.json index 8229d9e98..f64660fdd 100644 --- a/public/locales/ja-JP/translations.json +++ b/public/locales/ja-JP/translations.json @@ -741,7 +741,6 @@ "account_page_asset_table_column_asset_class": null, "account_page_asset_table_column_balance": null, "account_page_asset_table_column_balance_usd": null, - "account_page_asset_table_column_circulating_supply": null, "account_page_asset_table_column_currency_code": null, "account_page_asset_table_column_frozen": null, "account_page_asset_table_column_highest_bid": null, diff --git a/public/locales/ko-KR/translations.json b/public/locales/ko-KR/translations.json index d02b1dd5c..cfeddbd31 100644 --- a/public/locales/ko-KR/translations.json +++ b/public/locales/ko-KR/translations.json @@ -741,7 +741,6 @@ "account_page_asset_table_column_asset_class": null, "account_page_asset_table_column_balance": null, "account_page_asset_table_column_balance_usd": null, - "account_page_asset_table_column_circulating_supply": null, "account_page_asset_table_column_currency_code": null, "account_page_asset_table_column_frozen": null, "account_page_asset_table_column_highest_bid": null, diff --git a/public/locales/my-MM/translations.json b/public/locales/my-MM/translations.json index c1ef818ab..b948a258d 100644 --- a/public/locales/my-MM/translations.json +++ b/public/locales/my-MM/translations.json @@ -741,7 +741,6 @@ "account_page_asset_table_column_asset_class": null, "account_page_asset_table_column_balance": null, "account_page_asset_table_column_balance_usd": null, - "account_page_asset_table_column_circulating_supply": null, "account_page_asset_table_column_currency_code": null, "account_page_asset_table_column_frozen": null, "account_page_asset_table_column_highest_bid": null, diff --git a/src/containers/Accounts/AccountAsset/assetTables/IssuedMPTs.tsx b/src/containers/Accounts/AccountAsset/assetTables/IssuedMPTs.tsx index 0b47cdd14..273837ab4 100644 --- a/src/containers/Accounts/AccountAsset/assetTables/IssuedMPTs.tsx +++ b/src/containers/Accounts/AccountAsset/assetTables/IssuedMPTs.tsx @@ -115,7 +115,7 @@ const IssuedMPTsContent = ({ accountId, onChange }: IssuedMPTsProps) => { {t('account_page_asset_table_column_token_id')} {t('account_page_asset_table_column_ticker')} {t('account_page_asset_table_column_price_usd')} - {t('account_page_asset_table_column_circulating_supply')} + {t('account_page_asset_table_column_supply')} {t('account_page_asset_table_column_confidential_balance')}{' '} diff --git a/src/containers/Accounts/AccountAsset/assetTables/NFTTable.tsx b/src/containers/Accounts/AccountAsset/assetTables/NFTTable.tsx index f3b3b37b7..1f19cd93e 100644 --- a/src/containers/Accounts/AccountAsset/assetTables/NFTTable.tsx +++ b/src/containers/Accounts/AccountAsset/assetTables/NFTTable.tsx @@ -16,9 +16,9 @@ import { XRP_BASE } from '../../../shared/transactionUtils' import { localizeNumber, shortenAccount, - shortenDomain, shortenNFTTokenID, } from '../../../shared/utils' +import { shortenDomain } from '../../../shared/domainUtils' import { XRP_SMALL_BALANCE_CURRENCY_OPTIONS } from '../../../shared/NumberFormattingUtils' import { useLanguage } from '../../../shared/hooks' import logger from '../../../../rippled/lib/logger' diff --git a/src/containers/Accounts/AccountAsset/test/IssuedMPTs.test.tsx b/src/containers/Accounts/AccountAsset/test/IssuedMPTs.test.tsx index c9190bc3e..b12ec04cf 100644 --- a/src/containers/Accounts/AccountAsset/test/IssuedMPTs.test.tsx +++ b/src/containers/Accounts/AccountAsset/test/IssuedMPTs.test.tsx @@ -90,7 +90,7 @@ describe('IssuedMPTs', () => { }) expect(screen.getByText('Ticker')).toBeInTheDocument() expect(screen.getByText('Price (USD)')).toBeInTheDocument() - expect(screen.getByText('Circ Supply')).toBeInTheDocument() + expect(screen.getByText('Supply')).toBeInTheDocument() expect(screen.getByText('Conf Balance')).toBeInTheDocument() expect(screen.getByText('Asset Class')).toBeInTheDocument() expect(screen.getByText('Transfer Fee')).toBeInTheDocument() diff --git a/src/containers/Token/IOU/Header/index.tsx b/src/containers/Token/IOU/Header/index.tsx index b826b5091..c2c883b7f 100644 --- a/src/containers/Token/IOU/Header/index.tsx +++ b/src/containers/Token/IOU/Header/index.tsx @@ -18,7 +18,11 @@ import { parsePercent, parsePrice, } from '../../../shared/NumberFormattingUtils' -import { shortenDomain, stripHttpProtocol } from '../../../shared/utils' +import { + getRegistrableDomain, + shortenDomainFromLeft, +} from '../../../shared/domainUtils' +import { calculateIouCirculatingSupply } from '../../shared/utils/circulatingSupply' interface HeaderProps { currency: string @@ -30,27 +34,6 @@ interface HeaderProps { isAmmTvlLoading: boolean } -const calculateCirculatingSupply = ( - tokenData: LOSToken, - holdersData: TokenHoldersData | undefined, -): number => { - if (tokenData.circ_supply) { - return Number(tokenData.circ_supply) - } - let circSupply = Number(tokenData.supply) || holdersData?.totalSupply || 0 - - // For stablecoins, don't subtract large percentage holders from circulating supply - if (tokenData.asset_subclass !== 'stablecoin' && holdersData) { - holdersData.holders.forEach((holder) => { - if (holder.percent >= 20) { - circSupply -= holder.balance - } - }) - } - - return circSupply -} - export const Header = ({ currency, tokenData, @@ -61,7 +44,7 @@ export const Header = ({ isAmmTvlLoading, }: HeaderProps) => { const { t } = useTranslation() - const circSupply = calculateCirculatingSupply(tokenData, holdersData) + const circSupply = calculateIouCirculatingSupply(tokenData, holdersData) const xrpRate = Number(xrpUSDRate) || 0 // Memoized formatted overview data @@ -93,11 +76,15 @@ export const Header = ({ const volume24hNum = Number(tokenData.daily_volume) || 0 const trades24hNum = Number(tokenData.daily_trades) || 0 - // Format supply values - const formattedSupply = parseAmount( - holdersData?.totalSupply?.toString() || tokenData.supply || '0', - ) - const formattedCircSupply = parseAmount(circSupplyNum) + // Format supply values; a zero supply isn't meaningful, so show "--" + const supplyValue = + holdersData?.totalSupply?.toString() || tokenData.supply || '0' + const formattedSupply = Number(supplyValue) + ? parseAmount(supplyValue) + : '--' + const formattedCircSupply = circSupplyNum + ? parseAmount(circSupplyNum) + : '--' // Calculate market cap let marketCap: string | null = null @@ -178,11 +165,10 @@ export const Header = ({ diff --git a/src/containers/Token/IOU/hooks/useMarketCalculations.ts b/src/containers/Token/IOU/hooks/useMarketCalculations.ts deleted file mode 100644 index 4323830ca..000000000 --- a/src/containers/Token/IOU/hooks/useMarketCalculations.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { useMemo } from 'react' -import { formatPrice } from '../../../shared/utils' -import { TokenHoldersData } from '../api/holders' -import { LOSToken } from '../../../shared/losTypes' -import { - calculateCirculatingSupply, - formatCirculatingSupply, -} from '../utils/tokenCalculations' - -interface MarketCalculations { - marketCap: string | null - circSupply: number - formattedCircSupply: string -} - -interface UseMarketCalculationsProps { - holdersData: TokenHoldersData | undefined - tokenData: LOSToken - price: string - xrpUSDRate: string - isHoldersDataLoading: boolean -} - -export const useMarketCalculations = ({ - holdersData, - tokenData, - price, - xrpUSDRate, - isHoldersDataLoading, -}: UseMarketCalculationsProps): MarketCalculations => - useMemo(() => { - const circSupply = calculateCirculatingSupply(holdersData, tokenData) - - const marketCap = - !isHoldersDataLoading && circSupply && price && xrpUSDRate - ? formatPrice( - Number(circSupply) * Number(price) * Number(xrpUSDRate), - ) || null - : null - - const formattedCircSupply = formatCirculatingSupply(circSupply) - - return { - marketCap, - circSupply, - formattedCircSupply, - } - }, [holdersData, tokenData, price, xrpUSDRate, isHoldersDataLoading]) diff --git a/src/containers/Token/IOU/test/Header/Header.test.tsx b/src/containers/Token/IOU/test/Header/Header.test.tsx index 5f048d197..e18436bbb 100644 --- a/src/containers/Token/IOU/test/Header/Header.test.tsx +++ b/src/containers/Token/IOU/test/Header/Header.test.tsx @@ -12,8 +12,12 @@ jest.mock('../../../../shared/components/Currency', () => ({ default: ({ currency }: { currency: string }) =>
{currency}
, })) +const mockHeaderBoxesSpy = jest.fn() jest.mock('../../components/HeaderBoxes', () => ({ - HeaderBoxes: () =>
HeaderBoxes
, + HeaderBoxes: (props: any) => { + mockHeaderBoxesSpy(props) + return
HeaderBoxes
+ }, })) jest.mock('../../../../shared/components/Account', () => ({ @@ -31,8 +35,6 @@ jest.mock('../../../../shared/utils', () => ({ return { num: d.toString(), unit: '' } }, formatSmallNumber: (value: number) => value.toFixed(4), - shortenDomain: (domain: string) => domain, - stripHttpProtocol: (url: string) => url.replace(/^https?:\/\//, ''), })) const TestWrapper = ({ children }: { children: React.ReactNode }) => ( @@ -377,4 +379,21 @@ describe('Header Component', () => { ) expect(screen.getByText('USD')).toBeInTheDocument() }) + it('shows -- instead of a formatted zero for supply and circ supply', () => { + mockHeaderBoxesSpy.mockClear() + render( + +
+ , + ) + const { marketData } = mockHeaderBoxesSpy.mock.calls.slice(-1)[0][0] + expect(marketData.supply).toBe('--') + expect(marketData.circ_supply).toBe('--') + }) }) diff --git a/src/containers/Token/IOU/test/hooks/useMarketCalculations.test.ts b/src/containers/Token/IOU/test/hooks/useMarketCalculations.test.ts deleted file mode 100644 index 040ffdf52..000000000 --- a/src/containers/Token/IOU/test/hooks/useMarketCalculations.test.ts +++ /dev/null @@ -1,273 +0,0 @@ -import { renderHook } from '@testing-library/react' -import { useMarketCalculations } from '../../hooks/useMarketCalculations' -import { TokenHoldersData } from '../../api/holders' -import { LOSToken } from '../../../../shared/losTypes' - -describe('useMarketCalculations', () => { - const mockTokenData: LOSToken = { - currency: 'USD', - issuer_account: 'rIssuer', - trustlines: 100, - index: 0, - } - - const mockHoldersData: TokenHoldersData = { - totalSupply: 1000000, - totalHolders: 100, - holders: [], - } - - it('returns market calculations with valid data', () => { - const { result } = renderHook(() => - useMarketCalculations({ - holdersData: mockHoldersData, - tokenData: { ...mockTokenData, supply: '1000000' }, - price: '0.5', - xrpUSDRate: '2', - isHoldersDataLoading: false, - }), - ) - - expect(result.current.circSupply).toBe(1000000) - expect(result.current.formattedCircSupply).toBeDefined() - expect(result.current.marketCap).toBeDefined() - }) - - it('returns null marketCap when isHoldersDataLoading is true', () => { - const { result } = renderHook(() => - useMarketCalculations({ - holdersData: mockHoldersData, - tokenData: { ...mockTokenData, supply: '1000000' }, - price: '0.5', - xrpUSDRate: '2', - isHoldersDataLoading: true, - }), - ) - - expect(result.current.marketCap).toBeNull() - }) - - it('returns null marketCap when circSupply is 0', () => { - const { result } = renderHook(() => - useMarketCalculations({ - holdersData: undefined, - tokenData: mockTokenData, - price: '0.5', - xrpUSDRate: '2', - isHoldersDataLoading: false, - }), - ) - - expect(result.current.marketCap).toBeNull() - }) - - it('returns null marketCap when price is empty', () => { - const { result } = renderHook(() => - useMarketCalculations({ - holdersData: mockHoldersData, - tokenData: { ...mockTokenData, supply: '1000000' }, - price: '', - xrpUSDRate: '2', - isHoldersDataLoading: false, - }), - ) - - expect(result.current.marketCap).toBeNull() - }) - - it('returns null marketCap when xrpUSDRate is empty', () => { - const { result } = renderHook(() => - useMarketCalculations({ - holdersData: mockHoldersData, - tokenData: { ...mockTokenData, supply: '1000000' }, - price: '0.5', - xrpUSDRate: '', - isHoldersDataLoading: false, - }), - ) - - expect(result.current.marketCap).toBeNull() - }) - - it('calculates marketCap correctly with valid inputs', () => { - const { result } = renderHook(() => - useMarketCalculations({ - holdersData: mockHoldersData, - tokenData: { ...mockTokenData, supply: '1000000' }, - price: '1', - xrpUSDRate: '2', - isHoldersDataLoading: false, - }), - ) - - // 1000000 * 1 * 2 = 2000000 - expect(result.current.marketCap).toBeDefined() - }) - - it('handles zero price', () => { - const { result } = renderHook(() => - useMarketCalculations({ - holdersData: mockHoldersData, - tokenData: { ...mockTokenData, supply: '1000000' }, - price: '0', - xrpUSDRate: '2', - isHoldersDataLoading: false, - }), - ) - - expect(result.current.marketCap).toBeNull() - }) - - it('handles zero xrpUSDRate', () => { - const { result } = renderHook(() => - useMarketCalculations({ - holdersData: mockHoldersData, - tokenData: { ...mockTokenData, supply: '1000000' }, - price: '0.5', - xrpUSDRate: '0', - isHoldersDataLoading: false, - }), - ) - - expect(result.current.marketCap).toBeNull() - }) - - it('handles very small price values', () => { - const { result } = renderHook(() => - useMarketCalculations({ - holdersData: mockHoldersData, - tokenData: { ...mockTokenData, supply: '1000000' }, - price: '0.0001', - xrpUSDRate: '2', - isHoldersDataLoading: false, - }), - ) - - expect(result.current.marketCap).toBeDefined() - }) - - it('handles very large price values', () => { - const { result } = renderHook(() => - useMarketCalculations({ - holdersData: mockHoldersData, - tokenData: { ...mockTokenData, supply: '1000000' }, - price: '1000000', - xrpUSDRate: '2', - isHoldersDataLoading: false, - }), - ) - - expect(result.current.marketCap).toBeDefined() - }) - - it('formats circulating supply', () => { - const { result } = renderHook(() => - useMarketCalculations({ - holdersData: mockHoldersData, - tokenData: { ...mockTokenData, supply: '1000000' }, - price: '0.5', - xrpUSDRate: '2', - isHoldersDataLoading: false, - }), - ) - - expect(result.current.formattedCircSupply).toBeDefined() - expect(typeof result.current.formattedCircSupply).toBe('string') - }) - - it('returns circSupply as number', () => { - const { result } = renderHook(() => - useMarketCalculations({ - holdersData: mockHoldersData, - tokenData: { ...mockTokenData, supply: '1000000' }, - price: '0.5', - xrpUSDRate: '2', - isHoldersDataLoading: false, - }), - ) - - expect(typeof result.current.circSupply).toBe('number') - }) - - it('handles undefined holdersData', () => { - const { result } = renderHook(() => - useMarketCalculations({ - holdersData: undefined, - tokenData: { ...mockTokenData, supply: '1000000' }, - price: '0.5', - xrpUSDRate: '2', - isHoldersDataLoading: false, - }), - ) - - expect(result.current.circSupply).toBe(1000000) - }) - - it('handles circ_supply in tokenData', () => { - const { result } = renderHook(() => - useMarketCalculations({ - holdersData: mockHoldersData, - tokenData: { ...mockTokenData, circ_supply: '500000' }, - price: '0.5', - xrpUSDRate: '2', - isHoldersDataLoading: false, - }), - ) - - expect(result.current.circSupply).toBe(500000) - }) - - it('memoizes results correctly', () => { - const { result, rerender } = renderHook( - (props) => useMarketCalculations(props), - { - initialProps: { - holdersData: mockHoldersData, - tokenData: { ...mockTokenData, supply: '1000000' }, - price: '0.5', - xrpUSDRate: '2', - isHoldersDataLoading: false, - }, - }, - ) - - const firstResult = result.current - - rerender({ - holdersData: mockHoldersData, - tokenData: { ...mockTokenData, supply: '1000000' }, - price: '0.5', - xrpUSDRate: '2', - isHoldersDataLoading: false, - }) - - expect(result.current).toEqual(firstResult) - }) - - it('updates when dependencies change', () => { - const { result, rerender } = renderHook( - (props) => useMarketCalculations(props), - { - initialProps: { - holdersData: mockHoldersData, - tokenData: { ...mockTokenData, supply: '1000000' }, - price: '0.5', - xrpUSDRate: '2', - isHoldersDataLoading: false, - }, - }, - ) - - const firstResult = result.current - - rerender({ - holdersData: mockHoldersData, - tokenData: { ...mockTokenData, supply: '2000000' }, - price: '0.5', - xrpUSDRate: '2', - isHoldersDataLoading: false, - }) - - expect(result.current.circSupply).not.toEqual(firstResult.circSupply) - }) -}) diff --git a/src/containers/Token/IOU/test/utils/tokenCalculations.test.ts b/src/containers/Token/IOU/test/utils/tokenCalculations.test.ts deleted file mode 100644 index 0e4701ccc..000000000 --- a/src/containers/Token/IOU/test/utils/tokenCalculations.test.ts +++ /dev/null @@ -1,217 +0,0 @@ -import { - calculateCirculatingSupply, - formatCirculatingSupply, -} from '../../utils/tokenCalculations' -import { TokenHoldersData } from '../../api/holders' -import { LOSToken } from '../../../../shared/losTypes' - -describe('tokenCalculations', () => { - describe('calculateCirculatingSupply', () => { - const mockTokenData: LOSToken = { - currency: 'USD', - issuer_account: 'rIssuer', - trustlines: 100, - index: 0, - } - - it('returns circ_supply from tokenData if available', () => { - const tokenData = { - ...mockTokenData, - circ_supply: '1000000', - } - const result = calculateCirculatingSupply(undefined, tokenData) - expect(result).toBe(1000000) - }) - - it('returns supply from tokenData when circ_supply is not available', () => { - const tokenData = { - ...mockTokenData, - supply: '5000000', - } - const result = calculateCirculatingSupply(undefined, tokenData) - expect(result).toBe(5000000) - }) - - it('returns totalSupply from holdersData when tokenData supply is not available', () => { - const holdersData: TokenHoldersData = { - totalSupply: 3000000, - totalHolders: 100, - holders: [], - } - const tokenData = { - ...mockTokenData, - } - const result = calculateCirculatingSupply(holdersData, tokenData) - expect(result).toBe(3000000) - }) - - it('returns 0 when no supply data is available', () => { - const tokenData = { - ...mockTokenData, - } - const result = calculateCirculatingSupply(undefined, tokenData) - expect(result).toBe(0) - }) - - it('subtracts large holders (>= 20%) for non-stablecoin tokens', () => { - const holdersData: TokenHoldersData = { - totalSupply: 1000000, - totalHolders: 5, - holders: [ - { account: 'rHolder1', balance: 300000, percent: 30 }, - { account: 'rHolder2', balance: 200000, percent: 20 }, - { account: 'rHolder3', balance: 100000, percent: 10 }, - ], - } - const tokenData = { - ...mockTokenData, - supply: '1000000', - asset_subclass: 'token', - } - const result = calculateCirculatingSupply(holdersData, tokenData) - // 1000000 - 300000 (30%) - 200000 (20%) = 500000 - expect(result).toBe(500000) - }) - - it('does not subtract large holders for stablecoin tokens', () => { - const holdersData: TokenHoldersData = { - totalSupply: 1000000, - totalHolders: 5, - holders: [ - { account: 'rHolder1', balance: 300000, percent: 30 }, - { account: 'rHolder2', balance: 200000, percent: 20 }, - { account: 'rHolder3', balance: 100000, percent: 10 }, - ], - } - const tokenData = { - ...mockTokenData, - supply: '1000000', - asset_subclass: 'stablecoin', - } - const result = calculateCirculatingSupply(holdersData, tokenData) - // For stablecoins, no subtraction: 1000000 - expect(result).toBe(1000000) - }) - - it('handles holders with exactly 20% threshold', () => { - const holdersData: TokenHoldersData = { - totalSupply: 1000000, - totalHolders: 5, - holders: [ - { account: 'rHolder1', balance: 200000, percent: 20 }, - { account: 'rHolder2', balance: 100000, percent: 10 }, - ], - } - const tokenData = { - ...mockTokenData, - supply: '1000000', - asset_subclass: 'token', - } - const result = calculateCirculatingSupply(holdersData, tokenData) - // 1000000 - 200000 (20%) = 800000 - expect(result).toBe(800000) - }) - - it('handles holders with less than 20% threshold', () => { - const holdersData: TokenHoldersData = { - totalSupply: 1000000, - totalHolders: 5, - holders: [ - { account: 'rHolder1', balance: 190000, percent: 19 }, - { account: 'rHolder2', balance: 100000, percent: 10 }, - ], - } - const tokenData = { - ...mockTokenData, - supply: '1000000', - asset_subclass: 'token', - } - const result = calculateCirculatingSupply(holdersData, tokenData) - // No subtraction: 1000000 - expect(result).toBe(1000000) - }) - - it('handles empty holders array', () => { - const holdersData: TokenHoldersData = { - totalSupply: 1000000, - totalHolders: 0, - holders: [], - } - const tokenData = { - ...mockTokenData, - supply: '1000000', - asset_subclass: 'token', - } - const result = calculateCirculatingSupply(holdersData, tokenData) - expect(result).toBe(1000000) - }) - - it('handles multiple large holders', () => { - const holdersData: TokenHoldersData = { - totalSupply: 1000000, - totalHolders: 10, - holders: [ - { account: 'rHolder1', balance: 250000, percent: 25 }, - { account: 'rHolder2', balance: 250000, percent: 25 }, - { account: 'rHolder3', balance: 250000, percent: 25 }, - { account: 'rHolder4', balance: 250000, percent: 25 }, - ], - } - const tokenData = { - ...mockTokenData, - supply: '1000000', - asset_subclass: 'token', - } - const result = calculateCirculatingSupply(holdersData, tokenData) - // 1000000 - (250000 * 4) = 0 - expect(result).toBe(0) - }) - - it('prioritizes circ_supply over supply', () => { - const holdersData: TokenHoldersData = { - totalSupply: 5000000, - totalHolders: 100, - holders: [], - } - const tokenData = { - ...mockTokenData, - circ_supply: '2000000', - supply: '3000000', - } - const result = calculateCirculatingSupply(holdersData, tokenData) - expect(result).toBe(2000000) - }) - }) - - describe('formatCirculatingSupply', () => { - it('formats small numbers with full precision', () => { - const result = formatCirculatingSupply(100) - expect(result).toBe('100') - }) - - it('formats large numbers with abbreviation', () => { - const result = formatCirculatingSupply(1000000) - expect(result).toMatch(/1\.0M|1M/) - }) - - it('formats very large numbers with abbreviation', () => { - const result = formatCirculatingSupply(1000000000) - expect(result).toMatch(/1\.0B|1B/) - }) - - it('formats zero', () => { - const result = formatCirculatingSupply(0) - expect(result).toBe('0') - }) - - it('formats decimal numbers', () => { - const result = formatCirculatingSupply(1234.5678) - expect(result).toBeDefined() - }) - - it('handles very small numbers', () => { - const result = formatCirculatingSupply(0.0001) - expect(result).toBeDefined() - }) - }) -}) diff --git a/src/containers/Token/IOU/utils/tokenCalculations.ts b/src/containers/Token/IOU/utils/tokenCalculations.ts deleted file mode 100644 index 350b204eb..000000000 --- a/src/containers/Token/IOU/utils/tokenCalculations.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { parseIntegerAmount } from '../../../shared/NumberFormattingUtils' -import { TokenHoldersData } from '../api/holders' -import { LOSToken } from '../../../shared/losTypes' - -/** - * Calculates the circulating supply for a token - * @param holdersData - Token holders data - * @param tokenData - Token data - * @returns Calculated circulating supply - */ -export const calculateCirculatingSupply = ( - holdersData: TokenHoldersData | undefined, - tokenData: LOSToken, -): number => { - if (tokenData.circ_supply) { - return Number(tokenData.circ_supply) - } - let circSupply = Number(tokenData.supply) || holdersData?.totalSupply || 0 - - // For stablecoins, don't subtract large percentage holders from circulating supply - if (tokenData.asset_subclass !== 'stablecoin' && holdersData) { - holdersData.holders.forEach((holder) => { - if (holder.percent >= 20) { - circSupply -= holder.balance - } - }) - } - - return circSupply -} - -/** - * Formats the circulating supply for display - * @param circSupply - The circulating supply number - * @returns Formatted circulating supply string - */ -export const formatCirculatingSupply = (circSupply: number): string => - parseIntegerAmount(circSupply) diff --git a/src/containers/Token/MPT/Header/GeneralOverview.tsx b/src/containers/Token/MPT/Header/GeneralOverview.tsx index aa3d8e242..a73e1df2b 100644 --- a/src/containers/Token/MPT/Header/GeneralOverview.tsx +++ b/src/containers/Token/MPT/Header/GeneralOverview.tsx @@ -36,10 +36,10 @@ export const GeneralOverview = ({
{t('token_page.issuer')}
-
+
@@ -60,7 +60,7 @@ export const GeneralOverview = ({
{t('token_page.transfer_fee')}
- {(transferFee && parsePercent(transferFee / 1000, 3)) ?? '--'} + {transferFee ? parsePercent(transferFee / 1000, 3) : '--'}
diff --git a/src/containers/Token/MPT/Header/MarketData.tsx b/src/containers/Token/MPT/Header/MarketData.tsx index 8fe2c314e..45250cb9b 100644 --- a/src/containers/Token/MPT/Header/MarketData.tsx +++ b/src/containers/Token/MPT/Header/MarketData.tsx @@ -5,28 +5,41 @@ import { Tooltip, useTooltip } from '../../../shared/components/Tooltip' import { ConfBalanceTooltipIcon } from '../../../shared/components/ConfBalanceTooltipIcon' interface MarketDataProps { - maxAmt?: string outstandingAmt?: string confidentialOutstandingAmt?: string assetScale?: number + // Circulating supply (outstanding minus large holders), scaled decimal string. + circulatingSupply?: string + circulatingSupplyLoading?: boolean } export const MarketData = ({ - maxAmt, outstandingAmt, confidentialOutstandingAmt, assetScale, + circulatingSupply, + circulatingSupplyLoading, }: MarketDataProps): JSX.Element => { const { t } = useTranslation() const { tooltip } = useTooltip() - const formattedSupply = parseAmount( - convertScaledPrice(BigInt(maxAmt || '0'), assetScale ?? 0), + // Supply is the on-chain outstanding amount. A zero supply isn't a meaningful + // figure, so show "--" rather than "0.00". + const scaledOutstanding = convertScaledPrice( + BigInt(outstandingAmt || '0'), + assetScale ?? 0, ) + const formattedSupply = Number(scaledOutstanding) + ? parseAmount(scaledOutstanding) + : '--' - const formattedCircSupply = parseAmount( - convertScaledPrice(BigInt(outstandingAmt || '0'), assetScale ?? 0), - ) + // Circulating supply is derived from holder data, and is undefined when that + // is unavailable (e.g. the holders request failed) — "--" either way, never + // the unadjusted supply, which would look like a real circulating figure. + const formattedCircSupply = + circulatingSupply && Number(circulatingSupply) + ? parseAmount(circulatingSupply) + : '--' const formattedConfidentialAmt = confidentialOutstandingAmt ? parseAmount( @@ -45,7 +58,13 @@ export const MarketData = ({
{t('token_page.circulating_supply')}
-
{formattedCircSupply}
+
+ {circulatingSupplyLoading ? ( + + ) : ( + formattedCircSupply + )} +
{formattedConfidentialAmt && (
diff --git a/src/containers/Token/MPT/Header/Metadata.tsx b/src/containers/Token/MPT/Header/Metadata.tsx index 2182abb3b..f37e86af2 100644 --- a/src/containers/Token/MPT/Header/Metadata.tsx +++ b/src/containers/Token/MPT/Header/Metadata.tsx @@ -1,5 +1,8 @@ import { useTranslation } from 'react-i18next' -import { JsonView } from '../../../shared/components/JsonView' +import { + JsonView, + NO_STRING_COLLAPSE, +} from '../../../shared/components/JsonView' interface MetadataProps { decodedMPTMetadata: Record | string @@ -22,7 +25,12 @@ export const Metadata = ({ {isString ? (
{decodedMPTMetadata}
) : ( - + )}
diff --git a/src/containers/Token/MPT/Header/index.tsx b/src/containers/Token/MPT/Header/index.tsx index 74bed94d9..00106edc7 100644 --- a/src/containers/Token/MPT/Header/index.tsx +++ b/src/containers/Token/MPT/Header/index.tsx @@ -5,12 +5,13 @@ import './styles.scss' import { BAD_REQUEST, HASH192_REGEX, - shortenDomain, shortenMPTID, - stripHttpProtocol, convertToHttpURL, - shortenAccount, } from '../../../shared/utils' +import { + getRegistrableDomain, + shortenDomainFromLeft, +} from '../../../shared/domainUtils' import { CopyableText } from '../../../shared/components/CopyableText' import DomainLink from '../../../shared/components/DomainLink' import { FormattedMPTIssuance } from '../../../shared/Interfaces' @@ -37,6 +38,8 @@ interface Props { setError: (error: number | null) => void holdersCount?: number holdersLoading?: boolean + circulatingSupply?: string + circulatingSupplyLoading?: boolean } export const Header = (props: Props) => { @@ -48,6 +51,8 @@ export const Header = (props: Props) => { setError, holdersCount, holdersLoading, + circulatingSupply, + circulatingSupplyLoading, } = props const [showURLDropdown, setShowURLDropdown] = useState(false) const dropdownRef = useRef(null) @@ -87,7 +92,6 @@ export const Header = (props: Props) => { const { issuer, assetScale, - maxAmt, outstandingAmt, confidentialOutstandingAmt, transferFee, @@ -162,13 +166,6 @@ export const Header = (props: Props) => { /> )} - {/* Show issuer name if available */} - {issuerName && ( -
- ({shortenAccount(issuerName)} - ) -
- )}
{allUris.length > 0 && ( @@ -181,11 +178,10 @@ export const Header = (props: Props) => { {allUris.length > 1 && (