mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
* add export for address txs * transfer all export from page to dialog * remove old route and components * improve file name and fix loading state of icon button * basic implementation * refetch item status * status indicator for download items * changes for multichain * handle too many requests for downloads error * update button styles and spacing * fix tests * disable async export for multichain * Fix CSV export cancel flow and stabilize context updates Applied via @cursor push command * test and review fixes * design fixes * refactor file names --------- Co-authored-by: Hanzo Dev <[email protected]>
309 lines
11 KiB
TypeScript
309 lines
11 KiB
TypeScript
import { Box } from '@chakra-ui/react';
|
|
import { useQueryClient } from '@tanstack/react-query';
|
|
import { useRouter } from 'next/router';
|
|
import React, { useEffect } from 'react';
|
|
|
|
import type { SocketMessage } from 'lib/socket/types';
|
|
import type { TabItemRegular } from 'toolkit/components/AdaptiveTabs/types';
|
|
import type { TokenInfo } from 'types/api/token';
|
|
import type { PaginationParams } from 'ui/shared/pagination/types';
|
|
|
|
import CsvExport from 'client/features/csv-export/components/CsvExport';
|
|
import config from 'configs/app';
|
|
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
|
|
import useIsMobile from 'lib/hooks/useIsMobile';
|
|
import * as metadata from 'lib/metadata';
|
|
import getQueryParamString from 'lib/router/getQueryParamString';
|
|
import useEtherscanRedirects from 'lib/router/useEtherscanRedirects';
|
|
import useSocketChannel from 'lib/socket/useSocketChannel';
|
|
import useSocketMessage from 'lib/socket/useSocketMessage';
|
|
import { NFT_TOKEN_TYPE_IDS } from 'lib/token/tokenTypes';
|
|
import * as addressStubs from 'stubs/address';
|
|
import * as tokenStubs from 'stubs/token';
|
|
import { getTokenHoldersStub } from 'stubs/token';
|
|
import { generateListStub } from 'stubs/utils';
|
|
import RoutedTabs from 'toolkit/components/RoutedTabs/RoutedTabs';
|
|
import Address3rdPartyWidgets from 'ui/address/Address3rdPartyWidgets';
|
|
import useAddress3rdPartyWidgets from 'ui/address/address3rdPartyWidgets/useAddress3rdPartyWidgets';
|
|
import AddressContract from 'ui/address/AddressContract';
|
|
import { CONTRACT_TAB_IDS } from 'ui/address/contract/utils';
|
|
import TextAd from 'ui/shared/ad/TextAd';
|
|
import IconSvg from 'ui/shared/IconSvg';
|
|
import Pagination from 'ui/shared/pagination/Pagination';
|
|
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
|
|
import TokenAdvancedFilterLink from 'ui/token/TokenAdvancedFilterLink';
|
|
import TokenDetails from 'ui/token/TokenDetails';
|
|
import TokenHolders from 'ui/token/TokenHolders/TokenHolders';
|
|
import TokenInventory from 'ui/token/TokenInventory';
|
|
import TokenPageTitle from 'ui/token/TokenPageTitle';
|
|
import TokenTransfer from 'ui/token/TokenTransfer/TokenTransfer';
|
|
import useTokenQuery from 'ui/token/useTokenQuery';
|
|
|
|
export type TokenTabs = 'token_transfers' | 'holders' | 'inventory';
|
|
|
|
const TABS_RIGHT_SLOT_PROPS = {
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
};
|
|
|
|
const TokenPageContent = () => {
|
|
const [ isQueryEnabled, setIsQueryEnabled ] = React.useState(false);
|
|
const [ totalSupplySocket, setTotalSupplySocket ] = React.useState<number>();
|
|
const router = useRouter();
|
|
const isMobile = useIsMobile();
|
|
|
|
const scrollRef = React.useRef<HTMLDivElement>(null);
|
|
|
|
const hashString = getQueryParamString(router.query.hash);
|
|
const tab = getQueryParamString(router.query.tab);
|
|
const ownerFilter = getQueryParamString(router.query.holder_address_hash) || undefined;
|
|
|
|
useEtherscanRedirects();
|
|
const queryClient = useQueryClient();
|
|
|
|
const tokenQuery = useTokenQuery(hashString);
|
|
|
|
const addressQuery = useApiQuery('general:address', {
|
|
pathParams: { hash: hashString },
|
|
queryOptions: {
|
|
enabled: isQueryEnabled && Boolean(router.query.hash),
|
|
placeholderData: addressStubs.ADDRESS_INFO,
|
|
},
|
|
});
|
|
|
|
React.useEffect(() => {
|
|
if (tokenQuery.data && totalSupplySocket) {
|
|
queryClient.setQueryData(getResourceKey('general:token', { pathParams: { hash: hashString } }), (prevData: TokenInfo | undefined) => {
|
|
if (prevData) {
|
|
return { ...prevData, total_supply: totalSupplySocket.toString() };
|
|
}
|
|
});
|
|
}
|
|
}, [ tokenQuery.data, totalSupplySocket, hashString, queryClient ]);
|
|
|
|
const handleTotalSupplyMessage: SocketMessage.TokenTotalSupply['handler'] = React.useCallback((payload) => {
|
|
const prevData = queryClient.getQueryData(getResourceKey('general:token', { pathParams: { hash: hashString } }));
|
|
if (!prevData) {
|
|
setTotalSupplySocket(payload.total_supply);
|
|
}
|
|
queryClient.setQueryData(getResourceKey('general:token', { pathParams: { hash: hashString } }), (prevData: TokenInfo | undefined) => {
|
|
if (prevData) {
|
|
return { ...prevData, total_supply: payload.total_supply.toString() };
|
|
}
|
|
});
|
|
}, [ queryClient, hashString ]);
|
|
|
|
const enableQuery = React.useCallback(() => setIsQueryEnabled(true), []);
|
|
|
|
const channel = useSocketChannel({
|
|
topic: `tokens:${ hashString?.toLowerCase() }`,
|
|
isDisabled: !hashString,
|
|
onJoin: enableQuery,
|
|
onSocketError: enableQuery,
|
|
});
|
|
useSocketMessage({
|
|
channel,
|
|
event: 'total_supply',
|
|
handler: handleTotalSupplyMessage,
|
|
});
|
|
|
|
const verifiedInfoQuery = useApiQuery('contractInfo:token_verified_info', {
|
|
pathParams: { hash: tokenQuery.data?.address_hash, chainId: config.chain.id },
|
|
queryOptions: { enabled: Boolean(tokenQuery.data) && !tokenQuery.isPlaceholderData && config.features.verifiedTokens.isEnabled },
|
|
});
|
|
|
|
useEffect(() => {
|
|
// even if config.meta.seo.enhancedDataEnabled is enabled, we don't fetch contract info for the project description
|
|
// so we need to update the metadata anyway.
|
|
if (tokenQuery.data && !tokenQuery.isPlaceholderData && !verifiedInfoQuery.isPlaceholderData) {
|
|
const apiData = {
|
|
...tokenQuery.data,
|
|
symbol_or_name: tokenQuery.data.symbol ?? tokenQuery.data.name ?? '',
|
|
description: verifiedInfoQuery.data?.projectDescription,
|
|
projectName: verifiedInfoQuery.data?.projectName,
|
|
};
|
|
metadata.update({ pathname: '/token/[hash]', query: { hash: tokenQuery.data.address_hash } }, apiData);
|
|
}
|
|
}, [ tokenQuery.data, tokenQuery.isPlaceholderData, verifiedInfoQuery.isPlaceholderData, verifiedInfoQuery.data ]);
|
|
|
|
const hasData = (tokenQuery.data && !tokenQuery.isPlaceholderData) && (addressQuery.data && !addressQuery.isPlaceholderData);
|
|
const hasInventoryTab = tokenQuery.data?.type && NFT_TOKEN_TYPE_IDS.includes(tokenQuery.data.type);
|
|
const isFirstTabTokenTransfer = !hasInventoryTab && !tab;
|
|
|
|
const transfersQuery = useQueryWithPages({
|
|
resourceName: 'general:token_transfers',
|
|
pathParams: { hash: hashString },
|
|
scrollRef,
|
|
options: {
|
|
enabled: Boolean(
|
|
hasData &&
|
|
hashString &&
|
|
(
|
|
(!hasInventoryTab && !tab) ||
|
|
tab === 'token_transfers'
|
|
),
|
|
),
|
|
placeholderData: tokenStubs.getTokenTransfersStub(tokenQuery.data?.type),
|
|
},
|
|
});
|
|
|
|
const inventoryQuery = useQueryWithPages({
|
|
resourceName: 'general:token_inventory',
|
|
pathParams: { hash: hashString },
|
|
filters: ownerFilter ? { holder_address_hash: ownerFilter } : {},
|
|
scrollRef,
|
|
options: {
|
|
enabled: Boolean(
|
|
hasData &&
|
|
hashString &&
|
|
(
|
|
(hasInventoryTab && !tab) ||
|
|
tab === 'inventory'
|
|
),
|
|
),
|
|
placeholderData: generateListStub<'general:token_inventory'>(tokenStubs.TOKEN_INSTANCE, 50, { next_page_params: { unique_token: 1 } }),
|
|
},
|
|
});
|
|
|
|
const holdersQuery = useQueryWithPages({
|
|
resourceName: 'general:token_holders',
|
|
pathParams: { hash: hashString },
|
|
scrollRef,
|
|
options: {
|
|
enabled: Boolean(hashString && tab === 'holders' && hasData),
|
|
placeholderData: getTokenHoldersStub(tokenQuery.data?.type, null),
|
|
},
|
|
});
|
|
|
|
const address3rdPartyWidgets = useAddress3rdPartyWidgets('token', false, isQueryEnabled);
|
|
|
|
const isLoading =
|
|
tokenQuery.isPlaceholderData ||
|
|
addressQuery.isPlaceholderData ||
|
|
(address3rdPartyWidgets.isEnabled && address3rdPartyWidgets.configQuery.isPlaceholderData);
|
|
|
|
const tabs: Array<TabItemRegular> = [
|
|
hasInventoryTab ? {
|
|
id: 'inventory',
|
|
title: 'Inventory',
|
|
component: <TokenInventory inventoryQuery={ inventoryQuery } tokenQuery={ tokenQuery } ownerFilter={ ownerFilter } shouldRender={ !isLoading }/>,
|
|
} : undefined,
|
|
{
|
|
id: 'token_transfers',
|
|
title: 'Token transfers',
|
|
component: <TokenTransfer transfersQuery={ transfersQuery } tokenQuery={ tokenQuery } shouldRender={ !isLoading }/>,
|
|
},
|
|
{
|
|
id: 'holders',
|
|
title: 'Holders',
|
|
component: <TokenHolders token={ tokenQuery.data } holdersQuery={ holdersQuery } shouldRender={ !isLoading }/>,
|
|
},
|
|
addressQuery.data?.is_contract ? {
|
|
id: 'contract',
|
|
title: () => {
|
|
if (addressQuery.data?.is_verified) {
|
|
return (
|
|
<>
|
|
<span>Contract</span>
|
|
<IconSvg name="status/success" boxSize="14px" color="green.500"/>
|
|
</>
|
|
);
|
|
}
|
|
|
|
return 'Contract';
|
|
},
|
|
component: <AddressContract addressData={ addressQuery.data } isLoading={ isLoading }/>,
|
|
subTabs: CONTRACT_TAB_IDS,
|
|
} : undefined,
|
|
(address3rdPartyWidgets.isEnabled && address3rdPartyWidgets.items.length > 0) ? {
|
|
id: 'widgets',
|
|
title: 'Widgets',
|
|
count: address3rdPartyWidgets.items.length,
|
|
component: <Address3rdPartyWidgets shouldRender={ !isLoading } addressType="token" showAll/>,
|
|
} : undefined,
|
|
].filter(Boolean);
|
|
|
|
let pagination: PaginationParams | undefined;
|
|
|
|
if (isFirstTabTokenTransfer || tab === 'token_transfers') {
|
|
pagination = transfersQuery.pagination;
|
|
}
|
|
|
|
if (router.query.tab === 'holders') {
|
|
pagination = holdersQuery.pagination;
|
|
}
|
|
|
|
// default tab for nfts is token inventory
|
|
if ((hasInventoryTab && !tab) || tab === 'inventory') {
|
|
pagination = inventoryQuery.pagination;
|
|
}
|
|
|
|
const tabListProps = React.useCallback(() => {
|
|
if (isMobile) {
|
|
return { mt: 8 };
|
|
}
|
|
|
|
return {
|
|
pt: 6,
|
|
pb: 6,
|
|
marginBottom: 0,
|
|
};
|
|
}, [ isMobile ]);
|
|
|
|
const tabsRightSlot = React.useMemo(() => {
|
|
if (isMobile) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{ (tab === 'token_transfers' || tab === '') && (
|
|
<TokenAdvancedFilterLink token={ tokenQuery.data } ml={ 6 }/>
|
|
) }
|
|
{ tab === 'holders' && (
|
|
<CsvExport
|
|
type="token_holders"
|
|
resourceName="general:token_csv_export_holders"
|
|
pathParams={{ hash: hashString }}
|
|
queryParams={{ from_period: null, to_period: null }}
|
|
extraParams={{ token_name: tokenQuery.data?.name || 'Unknown token' }}
|
|
periodFilter={ false }
|
|
loadingInitial={ pagination?.isLoading }
|
|
ml={ 6 }
|
|
/>
|
|
) }
|
|
{ pagination?.isVisible && <Pagination ml={ 6 } { ...pagination }/> }
|
|
</>
|
|
);
|
|
}, [ hashString, isMobile, pagination, tab, tokenQuery.data ]);
|
|
|
|
return (
|
|
<>
|
|
<TextAd mb={ 6 }/>
|
|
|
|
<TokenPageTitle
|
|
tokenQuery={ tokenQuery }
|
|
addressQuery={ addressQuery }
|
|
verifiedInfoQuery={ verifiedInfoQuery }
|
|
hash={ hashString }
|
|
/>
|
|
|
|
<TokenDetails tokenQuery={ tokenQuery }/>
|
|
|
|
{ /* should stay before tabs to scroll up with pagination */ }
|
|
<Box ref={ scrollRef }></Box>
|
|
|
|
<RoutedTabs
|
|
tabs={ tabs }
|
|
listProps={ tabListProps }
|
|
rightSlot={ tabsRightSlot }
|
|
rightSlotProps={ TABS_RIGHT_SLOT_PROPS }
|
|
stickyEnabled={ !isMobile }
|
|
isLoading={ isLoading }
|
|
/>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default TokenPageContent;
|