Skip to content

Commit 3cfd0cf

Browse files
authored
Fix missed slot status and add auto-refetch for live epochs (#295)
- Only show "Missed" status when datasource has processed beyond that slot - Show "Pending" status for slots still being processed by datasource - Add timestampToSlot utility to convert bounds from timestamps to slots - Auto-refetch epoch data every 12s for current epoch (live) - Skip auto-refetch for historical epochs (improves performance)
1 parent 0aa2f54 commit 3cfd0cf

File tree

4 files changed

+83
-5
lines changed

4 files changed

+83
-5
lines changed

src/pages/ethereum/epochs/DetailPage.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { Tab } from '@/components/Navigation/Tab';
2323
import { ScrollableTabs } from '@/components/Navigation/ScrollableTabs';
2424
import { formatEpoch } from '@/utils';
2525
import { weiToEth } from '@/utils/ethereum';
26+
import { useBeaconClock } from '@/hooks/useBeaconClock';
2627
import { useNetworkChangeRedirect } from '@/hooks/useNetworkChangeRedirect';
2728
import { useTabState } from '@/hooks/useTabState';
2829
import { Route } from '@/routes/ethereum/epochs/$epoch';
@@ -52,8 +53,12 @@ export function DetailPage(): React.JSX.Element {
5253
const parsed = parseInt(params.epoch, 10);
5354
const epoch = isNaN(parsed) || parsed < 0 ? null : parsed;
5455

56+
// Determine if this is the current epoch (for auto-refetch)
57+
const { epoch: currentEpoch } = useBeaconClock();
58+
const isLiveEpoch = epoch !== null && epoch === currentEpoch;
59+
5560
// Fetch data for this epoch
56-
const { data, isLoading, error } = useEpochDetailData(epoch ?? 0);
61+
const { data, isLoading, error } = useEpochDetailData(epoch ?? 0, isLiveEpoch);
5762

5863
// Keyboard navigation
5964
useEffect(() => {

src/pages/ethereum/epochs/components/EpochSlotsTable/EpochSlotsTable.tsx

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import { Table } from '@/components/Lists/Table';
77
import type { Column } from '@/components/Lists/Table/Table.types';
88
import { Timestamp } from '@/components/DataDisplay/Timestamp';
99
import { useBeaconClock } from '@/hooks/useBeaconClock';
10+
import { useTablesBounds } from '@/hooks/useBounds';
11+
import { useNetwork } from '@/hooks/useNetwork';
12+
import { timestampToSlot } from '@/utils/beacon';
1013
import type { SlotData } from '../../hooks/useEpochDetailData.types';
1114
import type { EpochSlotsTableProps } from './EpochSlotsTable.types';
1215

@@ -17,10 +20,17 @@ import type { EpochSlotsTableProps } from './EpochSlotsTable.types';
1720
* - Slot number with slot-in-epoch subtext (clickable link to slot detail)
1821
* - Relative timestamp
1922
* - Proposer details (index and entity)
20-
* - Block status (canonical, missed)
23+
* - Block status (canonical, proposed, missed, pending, scheduled)
2124
* - Blob count
2225
* - Attestation metrics (head correct count, participation %)
2326
*
27+
* Status determination:
28+
* - Canonical: Slot appears in canonical chain (int_block_canonical)
29+
* - Proposed: Slot appears in head chain but not canonical (fct_block_head)
30+
* - Missed: Slot is missing from both chains AND datasources have processed beyond this slot
31+
* - Pending: Slot appears to be missing but datasources haven't processed it yet
32+
* - Scheduled: Future slot (beyond current beacon clock slot)
33+
*
2434
* Users can click on slot numbers to navigate to slot detail pages.
2535
* Timestamp component has interactive popup for more details.
2636
*/
@@ -31,10 +41,26 @@ export function EpochSlotsTable({
3141
sortOrder = 'asc',
3242
}: EpochSlotsTableProps): JSX.Element {
3343
const { slot: currentSlot } = useBeaconClock();
44+
const { currentNetwork } = useNetwork();
45+
46+
// Fetch bounds for block tables to determine if we can confidently mark slots as missed
47+
// Use both int_block_canonical and fct_block_head since status is determined by presence in either
48+
const { data: boundsData } = useTablesBounds(['int_block_canonical', 'fct_block_head']);
3449

3550
// Disable real-time features for historical views (prevents re-renders every 12s)
3651
const effectiveCurrentSlot = enableRealtimeHighlighting ? currentSlot : Number.MAX_SAFE_INTEGER;
3752

53+
// Determine the maximum slot we've processed in our datasources
54+
// Use minOfMaxes to get the earliest ending point across both tables
55+
// This ensures we only mark slots as missed if BOTH tables have processed beyond that slot
56+
// Bounds are returned as Unix timestamps, so convert to slot number
57+
const maxProcessedSlot = useMemo(() => {
58+
if (!boundsData?.aggregate.minOfMaxes || !currentNetwork) {
59+
return undefined;
60+
}
61+
return timestampToSlot(boundsData.aggregate.minOfMaxes, currentNetwork.genesis_time);
62+
}, [boundsData?.aggregate.minOfMaxes, currentNetwork]);
63+
3864
/**
3965
* Sort slots by slot number
4066
*/
@@ -122,13 +148,27 @@ export function EpochSlotsTable({
122148
</Badge>
123149
);
124150
}
151+
152+
// Only show "Missed" if we're confident the datasource has processed this slot
153+
// If maxProcessedSlot is undefined or the slot hasn't been processed yet, show "Pending"
125154
if (row.status === 'missed') {
155+
const hasBeenProcessed = maxProcessedSlot !== undefined && row.slot <= maxProcessedSlot;
156+
157+
if (!hasBeenProcessed) {
158+
return (
159+
<Badge color="gray" variant="border">
160+
Pending
161+
</Badge>
162+
);
163+
}
164+
126165
return (
127166
<Badge color="yellow" variant="border">
128167
Missed
129168
</Badge>
130169
);
131170
}
171+
132172
return (
133173
<Badge color="gray" variant="border">
134174
{row.status}
@@ -158,7 +198,7 @@ export function EpochSlotsTable({
158198
},
159199
},
160200
],
161-
[effectiveCurrentSlot, showSlotInEpoch]
201+
[effectiveCurrentSlot, showSlotInEpoch, maxProcessedSlot]
162202
);
163203

164204
/**

src/pages/ethereum/epochs/hooks/useEpochDetailData.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
intBlockCanonicalServiceListOptions,
1414
} from '@/api/@tanstack/react-query.gen';
1515
import { useNetwork } from '@/hooks/useNetwork';
16-
import { epochToTimestamp, getEpochSlotRange, slotToTimestamp } from '@/utils/beacon';
16+
import { epochToTimestamp, getEpochSlotRange, slotToTimestamp, SECONDS_PER_SLOT } from '@/utils/beacon';
1717

1818
import type {
1919
EpochDetailData,
@@ -35,15 +35,23 @@ import type {
3535
* - Block first seen times
3636
* - Attestation liveness by entity
3737
*
38+
* For live epochs (current epoch), data is automatically refetched every 12 seconds
39+
* to capture new blocks and attestations as they arrive.
40+
*
3841
* @param epoch - Epoch number
42+
* @param isLive - Whether this is the current epoch (enables auto-refetch)
3943
* @returns Comprehensive epoch data including slots, stats, and missed attestations
4044
*/
41-
export function useEpochDetailData(epoch: number): UseEpochDetailDataReturn {
45+
export function useEpochDetailData(epoch: number, isLive = false): UseEpochDetailDataReturn {
4246
const { currentNetwork } = useNetwork();
4347

4448
const epochStartTime = currentNetwork ? epochToTimestamp(epoch, currentNetwork.genesis_time) : 0;
4549
const { firstSlot, lastSlot } = getEpochSlotRange(epoch);
4650

51+
// For live epochs, refetch every slot (12 seconds) to capture new data as it arrives
52+
// For historical epochs, disable auto-refetch (false)
53+
const refetchInterval = isLive ? SECONDS_PER_SLOT * 1000 : false;
54+
4755
// Fetch data for all slots in the epoch using range queries (9 total queries)
4856
const results = useQueries({
4957
queries: [
@@ -58,6 +66,7 @@ export function useEpochDetailData(epoch: number): UseEpochDetailDataReturn {
5866
},
5967
}),
6068
enabled: !!currentNetwork && firstSlot >= 0,
69+
refetchInterval,
6170
},
6271
// 2. Blob counts
6372
{
@@ -69,6 +78,7 @@ export function useEpochDetailData(epoch: number): UseEpochDetailDataReturn {
6978
},
7079
}),
7180
enabled: !!currentNetwork && firstSlot >= 0,
81+
refetchInterval,
7282
},
7383
// 3. Attestation correctness
7484
{
@@ -80,6 +90,7 @@ export function useEpochDetailData(epoch: number): UseEpochDetailDataReturn {
8090
},
8191
}),
8292
enabled: !!currentNetwork && firstSlot >= 0,
93+
refetchInterval,
8394
},
8495
// 4. Proposer entities
8596
{
@@ -91,6 +102,7 @@ export function useEpochDetailData(epoch: number): UseEpochDetailDataReturn {
91102
},
92103
}),
93104
enabled: !!currentNetwork && firstSlot >= 0,
105+
refetchInterval,
94106
},
95107
// 5. Block first seen by nodes (get all, will filter to earliest per slot)
96108
{
@@ -103,6 +115,7 @@ export function useEpochDetailData(epoch: number): UseEpochDetailDataReturn {
103115
},
104116
}),
105117
enabled: !!currentNetwork && firstSlot >= 0,
118+
refetchInterval,
106119
},
107120
// 6. Attestation liveness by entity (only missed)
108121
{
@@ -115,6 +128,7 @@ export function useEpochDetailData(epoch: number): UseEpochDetailDataReturn {
115128
},
116129
}),
117130
enabled: !!currentNetwork && firstSlot >= 0,
131+
refetchInterval,
118132
},
119133
// 7. MEV data
120134
{
@@ -126,6 +140,7 @@ export function useEpochDetailData(epoch: number): UseEpochDetailDataReturn {
126140
},
127141
}),
128142
enabled: !!currentNetwork && firstSlot >= 0,
143+
refetchInterval,
129144
},
130145
// 8. Block head data (for gas, transactions, base fee)
131146
{
@@ -137,6 +152,7 @@ export function useEpochDetailData(epoch: number): UseEpochDetailDataReturn {
137152
},
138153
}),
139154
enabled: !!currentNetwork && firstSlot >= 0,
155+
refetchInterval,
140156
},
141157
// 9. Canonical blocks (for determining canonical vs proposed status)
142158
{
@@ -150,6 +166,7 @@ export function useEpochDetailData(epoch: number): UseEpochDetailDataReturn {
150166
},
151167
}),
152168
enabled: !!currentNetwork && firstSlot >= 0,
169+
refetchInterval,
153170
},
154171
],
155172
});

src/utils/beacon.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,22 @@ export function slotToTimestamp(slot: number, genesisTime: number): number {
7777
return genesisTime + slot * SECONDS_PER_SLOT;
7878
}
7979

80+
/**
81+
* Convert Unix timestamp to slot number
82+
*
83+
* @param timestamp - Unix timestamp in seconds
84+
* @param genesisTime - Genesis time in Unix seconds
85+
* @returns Beacon chain slot number
86+
*
87+
* @example
88+
* ```tsx
89+
* timestampToSlot(1606825223, 1606824023) // Returns 100
90+
* ```
91+
*/
92+
export function timestampToSlot(timestamp: number, genesisTime: number): number {
93+
return Math.floor((timestamp - genesisTime) / SECONDS_PER_SLOT);
94+
}
95+
8096
/**
8197
* Convert slot number to epoch number
8298
*

0 commit comments

Comments
 (0)