diff --git a/apps/explorer/src/app/components/epoch-overview/epoch-missing.tsx b/apps/explorer/src/app/components/epoch-overview/epoch-missing.tsx index 5454b6fdf..b6738d119 100644 --- a/apps/explorer/src/app/components/epoch-overview/epoch-missing.tsx +++ b/apps/explorer/src/app/components/epoch-overview/epoch-missing.tsx @@ -10,15 +10,30 @@ export type EpochMissingOverviewProps = { }; /** + * Renders a set of details for an epoch that has no representation in the + * data node. This is primarily for one of two reasons: + * + * 1. The epoch hasn't happened yet + * 2. The epoch happened before a snapshot, and thus the details don't exist + * + * This component is used when the API has responded with no data for an epoch + * by ID, so we already know that we can't display start time/block etc. + * + * We can detect 1 if the epoch is a higher number than the current epoch + * We can detect 2 if the epoch is in the past, but we still get no response. */ const EpochMissingOverview = ({ missingEpochId, }: EpochMissingOverviewProps) => { const { data, error, loading } = useExplorerFutureEpochQuery(); + // This should not happen, but it's easily handled if (!missingEpochId) { return -; } + + // No data should also not happen - we've requested the current epoch. This + // could happen at chain restart, but shouldn't. If it does, fallback. if (!data || loading || error) { return {missingEpochId}; } @@ -27,9 +42,11 @@ const EpochMissingOverview = ({ // Let's assume it is let isInFuture = true; + // Blank string will be return 0 seconds from getSecondsFromInterval const epochLength = data.networkParameter?.value || ''; - const epochLengthInSeconds = getSeconds(epochLength); + const epochLengthInSeconds = getSecondsFromInterval(epochLength); + // If we have enough information to predict a future or past block time, let's do it if (missingEpochId && data.epoch.id && data.epoch.timestamps.start) { const missing = parseInt(missingEpochId); const current = parseInt(data.epoch.id); @@ -65,7 +82,15 @@ const EpochMissingOverview = ({ export default EpochMissingOverview; -function getSeconds(str: string) { +/** + * Parses the interval string we get for the epoch length from the + * network parameter API. These are in the format '1D2H3m' for 1 day, + * 2 hours and 3 minutes. + * + * @param str Interval string + * @returns integer the number of seconds the interval represents + */ +export function getSecondsFromInterval(str: string) { let seconds = 0; const months = str.match(/(\d+)\s*M/); const days = str.match(/(\d+)\s*D/); diff --git a/apps/explorer/src/app/components/epoch-overview/epoch.tsx b/apps/explorer/src/app/components/epoch-overview/epoch.tsx index 332bc81ff..2cf8d84e3 100644 --- a/apps/explorer/src/app/components/epoch-overview/epoch.tsx +++ b/apps/explorer/src/app/components/epoch-overview/epoch.tsx @@ -17,6 +17,12 @@ export type EpochOverviewProps = { }; /** + * Displays detailed information about an epoch, given an ID. This + * works for past epochs and current epochs - future epochs, and a + * few other situations (see epoch-missing) will not return us + * enough information to render this. + * + * The details are hidden in a tooltip, behind the epoch number */ const EpochOverview = ({ id }: EpochOverviewProps) => { const { data, error, loading } = useExplorerEpochQuery({ @@ -96,6 +102,11 @@ export type IconForEpochProps = { end: string; }; +/** + * Chooses an icon to display next to the epoch number, representing + * when the epoch is relative to now (i.e. not yet started, started, + * finished) + */ function IconForEpoch({ start, end }: IconForEpochProps) { const startHasPassed = isPast(new Date(start)); const endHasPassed = end ? isPast(new Date(end)) : false; diff --git a/apps/explorer/src/app/components/txs/details/shared/tx-details-shared.tsx b/apps/explorer/src/app/components/txs/details/shared/tx-details-shared.tsx index 68a0463fb..e7c892afc 100644 --- a/apps/explorer/src/app/components/txs/details/shared/tx-details-shared.tsx +++ b/apps/explorer/src/app/components/txs/details/shared/tx-details-shared.tsx @@ -14,10 +14,14 @@ interface TxDetailsSharedProps { txData: BlockExplorerTransactionResult | undefined; pubKey: string | undefined; blockData: TendermintBlocksResponse | undefined; + + // A transitional property used in some complex TX types to display more detailed type information + // than the shared component can derive + hideTypeRow?: boolean; } // Applied to all header cells -const sharedHeaderProps = { +export const sharedHeaderProps = { // Ensures that multi line contents still have the header aligned to the first line className: 'align-top', }; @@ -31,6 +35,7 @@ export const TxDetailsShared = ({ txData, pubKey, blockData, + hideTypeRow = false, }: TxDetailsSharedProps) => { if (!txData) { return <>{t('Awaiting Block Explorer transaction details')}; @@ -41,10 +46,12 @@ export const TxDetailsShared = ({ return ( <> - - {t('Type')} - {txData.type} - + {hideTypeRow === false ? ( + + {t('Type')} + {txData.type} + + ) : null} {t('Hash')} diff --git a/apps/explorer/src/app/components/txs/details/tx-transfer.tsx b/apps/explorer/src/app/components/txs/details/tx-transfer.tsx index fb269b872..955677399 100644 --- a/apps/explorer/src/app/components/txs/details/tx-transfer.tsx +++ b/apps/explorer/src/app/components/txs/details/tx-transfer.tsx @@ -1,13 +1,20 @@ import { t } from '@vegaprotocol/react-helpers'; import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response'; -import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response'; -import { TxDetailsShared } from './shared/tx-details-shared'; +import type { + Block, + TendermintBlocksResponse, +} from '../../../routes/blocks/tendermint-blocks-response'; +import { sharedHeaderProps, TxDetailsShared } from './shared/tx-details-shared'; import { TableRow, TableCell, TableWithTbody } from '../../table'; import type { components } from '../../../../types/explorer'; import { PartyLink } from '../../links'; import SizeInAsset from '../../size-in-asset/size-in-asset'; import { TransferRecurring } from './transfer/transfer-recurring'; +import { + SPECIAL_CASE_NETWORK, + SPECIAL_CASE_NETWORK_ID, +} from '../../links/party-link/party-link'; type Transfer = components['schemas']['commandsv1Transfer']; @@ -45,10 +52,15 @@ export const TxDetailsTransfer = ({ return ( <> + + {t('Type')} + {getTypeLabelForTransfer(transfer)} + {from ? ( @@ -79,3 +91,24 @@ export const TxDetailsTransfer = ({ ); }; + +/** + * Gets a string description of this transfer + * @param txData A full transfer + * @returns string Transfer label + */ +export function getTypeLabelForTransfer(tx: Transfer) { + if (tx.to === SPECIAL_CASE_NETWORK || tx.to === SPECIAL_CASE_NETWORK_ID) { + if (tx.recurring && tx.recurring.dispatchStrategy) { + return 'Reward top up transfer'; + } + // Else: we don't know that it's a reward transfer, so let's not guess + } else if (tx.recurring) { + return 'Recurring transfer'; + } else if (tx.oneOff) { + // Currently redundant, but could be used to indicate something more specific + return 'Transfer'; + } + + return 'Transfer'; +}