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';
+}