Freshness diagnostics API, UI trust notes, mission control/stats updates, and deploy scripts.
Made-with: Cursor
This commit is contained in:
@@ -8,6 +8,12 @@ import { readWatchlistFromStorage } from '@/utils/watchlist'
|
||||
import PageIntro from '@/components/common/PageIntro'
|
||||
import { fetchPublicJson } from '@/utils/publicExplorer'
|
||||
import { normalizeTransaction } from '@/services/api/blockscout'
|
||||
import { summarizeChainActivity } from '@/utils/activityContext'
|
||||
import ActivityContextPanel from '@/components/common/ActivityContextPanel'
|
||||
import FreshnessTrustNote from '@/components/common/FreshnessTrustNote'
|
||||
import { normalizeExplorerStats, type ExplorerStats } from '@/services/api/stats'
|
||||
import type { MissionControlBridgeStatusResponse } from '@/services/api/missionControl'
|
||||
import { resolveEffectiveFreshness } from '@/utils/explorerFreshness'
|
||||
|
||||
function normalizeAddress(value: string) {
|
||||
const trimmed = value.trim()
|
||||
@@ -16,6 +22,9 @@ function normalizeAddress(value: string) {
|
||||
|
||||
interface AddressesPageProps {
|
||||
initialRecentTransactions: Transaction[]
|
||||
initialLatestBlocks: Array<{ number: number; timestamp: string }>
|
||||
initialStats: ExplorerStats | null
|
||||
initialBridgeStatus: MissionControlBridgeStatusResponse | null
|
||||
}
|
||||
|
||||
function serializeRecentTransactions(transactions: Transaction[]): Transaction[] {
|
||||
@@ -26,17 +35,43 @@ function serializeRecentTransactions(transactions: Transaction[]): Transaction[]
|
||||
block_number: transaction.block_number,
|
||||
from_address: transaction.from_address,
|
||||
to_address: transaction.to_address ?? null,
|
||||
created_at: transaction.created_at,
|
||||
})),
|
||||
),
|
||||
) as Transaction[]
|
||||
}
|
||||
|
||||
export default function AddressesPage({ initialRecentTransactions }: AddressesPageProps) {
|
||||
export default function AddressesPage({
|
||||
initialRecentTransactions,
|
||||
initialLatestBlocks,
|
||||
initialStats,
|
||||
initialBridgeStatus,
|
||||
}: AddressesPageProps) {
|
||||
const router = useRouter()
|
||||
const chainId = parseInt(process.env.NEXT_PUBLIC_CHAIN_ID || '138')
|
||||
const [query, setQuery] = useState('')
|
||||
const [recentTransactions, setRecentTransactions] = useState<Transaction[]>(initialRecentTransactions)
|
||||
const [watchlist, setWatchlist] = useState<string[]>([])
|
||||
const activityContext = useMemo(
|
||||
() =>
|
||||
summarizeChainActivity({
|
||||
blocks: initialLatestBlocks.map((block) => ({
|
||||
chain_id: chainId,
|
||||
number: block.number,
|
||||
hash: '',
|
||||
timestamp: block.timestamp,
|
||||
miner: '',
|
||||
transaction_count: 0,
|
||||
gas_used: 0,
|
||||
gas_limit: 0,
|
||||
})),
|
||||
transactions: recentTransactions,
|
||||
latestBlockNumber: initialLatestBlocks[0]?.number ?? null,
|
||||
latestBlockTimestamp: initialLatestBlocks[0]?.timestamp ?? null,
|
||||
freshness: resolveEffectiveFreshness(initialStats, initialBridgeStatus),
|
||||
}),
|
||||
[chainId, initialBridgeStatus, initialLatestBlocks, initialStats, recentTransactions],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (initialRecentTransactions.length > 0) {
|
||||
@@ -111,6 +146,17 @@ export default function AddressesPage({ initialRecentTransactions }: AddressesPa
|
||||
]}
|
||||
/>
|
||||
|
||||
<div className="mb-6">
|
||||
<ActivityContextPanel context={activityContext} title="Recent Address Activity Context" />
|
||||
<FreshnessTrustNote
|
||||
className="mt-3"
|
||||
context={activityContext}
|
||||
stats={initialStats}
|
||||
bridgeStatus={initialBridgeStatus}
|
||||
scopeLabel="Recently active addresses are derived from the latest visible indexed transactions."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Card className="mb-6" title="Open An Address">
|
||||
<form onSubmit={handleOpenAddress} className="flex flex-col gap-3 md:flex-row">
|
||||
<input
|
||||
@@ -158,7 +204,7 @@ export default function AddressesPage({ initialRecentTransactions }: AddressesPa
|
||||
<Card title="Recently Active Addresses">
|
||||
{activeAddresses.length === 0 ? (
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Recent address activity is unavailable right now. You can still open an address directly above.
|
||||
Recent address activity is unavailable in the latest visible transaction sample. You can still open an address directly above.
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
@@ -177,14 +223,30 @@ export default function AddressesPage({ initialRecentTransactions }: AddressesPa
|
||||
|
||||
export const getServerSideProps: GetServerSideProps<AddressesPageProps> = async () => {
|
||||
const chainId = Number(process.env.NEXT_PUBLIC_CHAIN_ID || '138')
|
||||
const transactionsResult = await fetchPublicJson<{ items?: unknown[] }>('/api/v2/transactions?page=1&page_size=20').catch(() => null)
|
||||
const [transactionsResult, blocksResult, statsResult, bridgeResult] = await Promise.all([
|
||||
fetchPublicJson<{ items?: unknown[] }>('/api/v2/transactions?page=1&page_size=20').catch(() => null),
|
||||
fetchPublicJson<{ items?: Array<{ height?: number | string | null; timestamp?: string | null }> }>('/api/v2/blocks?page=1&page_size=3').catch(() => null),
|
||||
fetchPublicJson<Record<string, unknown>>('/api/v2/stats').catch(() => null),
|
||||
fetchPublicJson<MissionControlBridgeStatusResponse>('/explorer-api/v1/track1/bridge/status').catch(() => null),
|
||||
])
|
||||
const initialRecentTransactions = Array.isArray(transactionsResult?.items)
|
||||
? transactionsResult.items.map((item) => normalizeTransaction(item as never, chainId))
|
||||
: []
|
||||
const initialLatestBlocks = Array.isArray(blocksResult?.items)
|
||||
? blocksResult.items
|
||||
.map((item) => ({
|
||||
number: Number(item.height || 0),
|
||||
timestamp: item.timestamp || '',
|
||||
}))
|
||||
.filter((item) => Number.isFinite(item.number) && item.number > 0 && item.timestamp)
|
||||
: []
|
||||
|
||||
return {
|
||||
props: {
|
||||
initialRecentTransactions: serializeRecentTransactions(initialRecentTransactions),
|
||||
initialLatestBlocks,
|
||||
initialStats: statsResult ? normalizeExplorerStats(statsResult as never) : null,
|
||||
initialBridgeStatus: bridgeResult,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,55 @@
|
||||
import type { GetServerSideProps } from 'next'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { blocksApi, Block } from '@/services/api/blocks'
|
||||
import { Card, Address } from '@/libs/frontend-ui-primitives'
|
||||
import Link from 'next/link'
|
||||
import PageIntro from '@/components/common/PageIntro'
|
||||
import { formatTimestamp } from '@/utils/format'
|
||||
import { fetchPublicJson } from '@/utils/publicExplorer'
|
||||
import { normalizeBlock } from '@/services/api/blockscout'
|
||||
import { normalizeBlock, normalizeTransaction } from '@/services/api/blockscout'
|
||||
import type { Transaction } from '@/services/api/transactions'
|
||||
import { transactionsApi } from '@/services/api/transactions'
|
||||
import { summarizeChainActivity } from '@/utils/activityContext'
|
||||
import ActivityContextPanel from '@/components/common/ActivityContextPanel'
|
||||
import FreshnessTrustNote from '@/components/common/FreshnessTrustNote'
|
||||
import { normalizeExplorerStats, type ExplorerStats } from '@/services/api/stats'
|
||||
import type { MissionControlBridgeStatusResponse } from '@/services/api/missionControl'
|
||||
import { resolveEffectiveFreshness, shouldExplainEmptyHeadBlocks } from '@/utils/explorerFreshness'
|
||||
|
||||
interface BlocksPageProps {
|
||||
initialBlocks: Block[]
|
||||
initialRecentTransactions: Transaction[]
|
||||
initialStats: ExplorerStats | null
|
||||
initialBridgeStatus: MissionControlBridgeStatusResponse | null
|
||||
}
|
||||
|
||||
export default function BlocksPage({ initialBlocks }: BlocksPageProps) {
|
||||
function serializeTransactions(transactions: Transaction[]): Transaction[] {
|
||||
return JSON.parse(
|
||||
JSON.stringify(
|
||||
transactions.map((transaction) => ({
|
||||
hash: transaction.hash,
|
||||
block_number: transaction.block_number,
|
||||
from_address: transaction.from_address,
|
||||
to_address: transaction.to_address ?? null,
|
||||
value: transaction.value,
|
||||
status: transaction.status ?? null,
|
||||
contract_address: transaction.contract_address ?? null,
|
||||
fee: transaction.fee ?? null,
|
||||
created_at: transaction.created_at,
|
||||
})),
|
||||
),
|
||||
) as Transaction[]
|
||||
}
|
||||
|
||||
export default function BlocksPage({
|
||||
initialBlocks,
|
||||
initialRecentTransactions,
|
||||
initialStats,
|
||||
initialBridgeStatus,
|
||||
}: BlocksPageProps) {
|
||||
const pageSize = 20
|
||||
const [blocks, setBlocks] = useState<Block[]>(initialBlocks)
|
||||
const [recentTransactions, setRecentTransactions] = useState<Transaction[]>(initialRecentTransactions)
|
||||
const [loading, setLoading] = useState(initialBlocks.length === 0)
|
||||
const [page, setPage] = useState(1)
|
||||
const chainId = parseInt(process.env.NEXT_PUBLIC_CHAIN_ID || '138')
|
||||
@@ -47,8 +82,43 @@ export default function BlocksPage({ initialBlocks }: BlocksPageProps) {
|
||||
void loadBlocks()
|
||||
}, [initialBlocks, loadBlocks, page])
|
||||
|
||||
useEffect(() => {
|
||||
if (initialRecentTransactions.length > 0) {
|
||||
setRecentTransactions(initialRecentTransactions)
|
||||
return
|
||||
}
|
||||
|
||||
let active = true
|
||||
transactionsApi.listSafe(chainId, 1, 5)
|
||||
.then(({ ok, data }) => {
|
||||
if (active && ok && data.length > 0) {
|
||||
setRecentTransactions(data)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (active) {
|
||||
setRecentTransactions([])
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
active = false
|
||||
}
|
||||
}, [chainId, initialRecentTransactions])
|
||||
|
||||
const showPagination = page > 1 || blocks.length > 0
|
||||
const canGoNext = blocks.length === pageSize
|
||||
const activityContext = useMemo(
|
||||
() =>
|
||||
summarizeChainActivity({
|
||||
blocks,
|
||||
transactions: recentTransactions,
|
||||
latestBlockNumber: blocks[0]?.number ?? null,
|
||||
latestBlockTimestamp: blocks[0]?.timestamp ?? null,
|
||||
freshness: resolveEffectiveFreshness(initialStats, initialBridgeStatus),
|
||||
}),
|
||||
[blocks, initialBridgeStatus, initialStats, recentTransactions],
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-6 sm:py-8">
|
||||
@@ -63,12 +133,30 @@ export default function BlocksPage({ initialBlocks }: BlocksPageProps) {
|
||||
]}
|
||||
/>
|
||||
|
||||
<div className="mb-6">
|
||||
<ActivityContextPanel context={activityContext} title="Block Production Context" />
|
||||
<FreshnessTrustNote
|
||||
className="mt-3"
|
||||
context={activityContext}
|
||||
stats={initialStats}
|
||||
bridgeStatus={initialBridgeStatus}
|
||||
scopeLabel="This page focuses on recent visible head blocks."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<Card>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">Loading blocks...</p>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{shouldExplainEmptyHeadBlocks(blocks, activityContext) ? (
|
||||
<Card className="border border-amber-200 bg-amber-50/70 dark:border-amber-900/40 dark:bg-amber-950/20">
|
||||
<p className="text-sm text-amber-900 dark:text-amber-100">
|
||||
Recent head blocks are currently empty; use the latest transaction block for recent visible activity.
|
||||
</p>
|
||||
</Card>
|
||||
) : null}
|
||||
{blocks.length === 0 ? (
|
||||
<Card>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">Recent blocks are unavailable right now.</p>
|
||||
@@ -161,13 +249,23 @@ export default function BlocksPage({ initialBlocks }: BlocksPageProps) {
|
||||
|
||||
export const getServerSideProps: GetServerSideProps<BlocksPageProps> = async () => {
|
||||
const chainId = Number(process.env.NEXT_PUBLIC_CHAIN_ID || '138')
|
||||
const blocksResult = await fetchPublicJson<{ items?: unknown[] }>('/api/v2/blocks?page=1&page_size=20').catch(() => null)
|
||||
const [blocksResult, transactionsResult, statsResult, bridgeResult] = await Promise.all([
|
||||
fetchPublicJson<{ items?: unknown[] }>('/api/v2/blocks?page=1&page_size=20').catch(() => null),
|
||||
fetchPublicJson<{ items?: unknown[] }>('/api/v2/transactions?page=1&page_size=5').catch(() => null),
|
||||
fetchPublicJson<Record<string, unknown>>('/api/v2/stats').catch(() => null),
|
||||
fetchPublicJson<MissionControlBridgeStatusResponse>('/explorer-api/v1/track1/bridge/status').catch(() => null),
|
||||
])
|
||||
|
||||
return {
|
||||
props: {
|
||||
initialBlocks: Array.isArray(blocksResult?.items)
|
||||
? blocksResult.items.map((item) => normalizeBlock(item as never, chainId))
|
||||
: [],
|
||||
initialRecentTransactions: Array.isArray(transactionsResult?.items)
|
||||
? serializeTransactions(transactionsResult.items.map((item) => normalizeTransaction(item as never, chainId)))
|
||||
: [],
|
||||
initialStats: statsResult ? normalizeExplorerStats(statsResult as never) : null,
|
||||
initialBridgeStatus: bridgeResult,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,12 @@ import PageIntro from '@/components/common/PageIntro'
|
||||
|
||||
const docsCards = [
|
||||
{
|
||||
title: 'GRU guide',
|
||||
title: 'GRU Guide',
|
||||
href: '/docs/gru',
|
||||
description: 'Understand GRU standards, x402 readiness, wrapped transport posture, and forward-canonical versioning as surfaced by the explorer.',
|
||||
},
|
||||
{
|
||||
title: 'Transaction evidence matrix',
|
||||
title: 'Transaction Evidence Matrix',
|
||||
href: '/docs/transaction-review',
|
||||
description: 'See how the explorer scores transaction evidence quality, decode richness, asset posture, and counterparty traceability.',
|
||||
},
|
||||
|
||||
@@ -11,16 +11,21 @@ import {
|
||||
} from '@/services/api/stats'
|
||||
import {
|
||||
summarizeMissionControlRelay,
|
||||
type MissionControlBridgeStatusResponse,
|
||||
type MissionControlRelaySummary,
|
||||
} from '@/services/api/missionControl'
|
||||
import type { Block } from '@/services/api/blocks'
|
||||
import type { Transaction } from '@/services/api/transactions'
|
||||
import { fetchPublicJson } from '@/utils/publicExplorer'
|
||||
import { normalizeTransaction } from '@/services/api/blockscout'
|
||||
|
||||
interface IndexPageProps {
|
||||
initialStats: ExplorerStats | null
|
||||
initialRecentBlocks: Block[]
|
||||
initialRecentTransactions: Transaction[]
|
||||
initialTransactionTrend: ExplorerTransactionTrendPoint[]
|
||||
initialActivitySnapshot: ExplorerRecentActivitySnapshot | null
|
||||
initialBridgeStatus: MissionControlBridgeStatusResponse | null
|
||||
initialRelaySummary: MissionControlRelaySummary | null
|
||||
}
|
||||
|
||||
@@ -28,10 +33,28 @@ export default function IndexPage(props: IndexPageProps) {
|
||||
return <HomePage {...props} />
|
||||
}
|
||||
|
||||
function serializeTransactions(transactions: Transaction[]): Transaction[] {
|
||||
return JSON.parse(
|
||||
JSON.stringify(
|
||||
transactions.map((transaction) => ({
|
||||
hash: transaction.hash,
|
||||
block_number: transaction.block_number,
|
||||
from_address: transaction.from_address,
|
||||
to_address: transaction.to_address ?? null,
|
||||
value: transaction.value,
|
||||
status: transaction.status ?? null,
|
||||
contract_address: transaction.contract_address ?? null,
|
||||
fee: transaction.fee ?? null,
|
||||
created_at: transaction.created_at,
|
||||
})),
|
||||
),
|
||||
) as Transaction[]
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps<IndexPageProps> = async () => {
|
||||
const chainId = Number(process.env.NEXT_PUBLIC_CHAIN_ID || '138')
|
||||
|
||||
const [statsResult, blocksResult, trendResult, activityResult, bridgeResult] = await Promise.allSettled([
|
||||
const [statsResult, blocksResult, transactionsResult, trendResult, activityResult, bridgeResult] = await Promise.allSettled([
|
||||
fetchPublicJson<{
|
||||
total_blocks?: number | string | null
|
||||
total_transactions?: number | string | null
|
||||
@@ -39,6 +62,7 @@ export const getServerSideProps: GetServerSideProps<IndexPageProps> = async () =
|
||||
latest_block?: number | string | null
|
||||
}>('/api/v2/stats'),
|
||||
fetchPublicJson<{ items?: unknown[] }>('/api/v2/blocks?page=1&page_size=10'),
|
||||
fetchPublicJson<{ items?: unknown[] }>('/api/v2/transactions?page=1&page_size=5'),
|
||||
fetchPublicJson<{ chart_data?: Array<{ date?: string | null; transaction_count?: number | string | null }> }>(
|
||||
'/api/v2/stats/charts/transactions'
|
||||
),
|
||||
@@ -60,10 +84,18 @@ export const getServerSideProps: GetServerSideProps<IndexPageProps> = async () =
|
||||
blocksResult.status === 'fulfilled' && Array.isArray(blocksResult.value?.items)
|
||||
? blocksResult.value.items.map((item) => normalizeBlock(item as never, chainId))
|
||||
: [],
|
||||
initialRecentTransactions:
|
||||
transactionsResult.status === 'fulfilled' && Array.isArray(transactionsResult.value?.items)
|
||||
? serializeTransactions(
|
||||
transactionsResult.value.items.map((item) => normalizeTransaction(item as never, chainId)),
|
||||
)
|
||||
: [],
|
||||
initialTransactionTrend:
|
||||
trendResult.status === 'fulfilled' ? normalizeTransactionTrend(trendResult.value) : [],
|
||||
initialActivitySnapshot:
|
||||
activityResult.status === 'fulfilled' ? summarizeRecentTransactions(activityResult.value) : null,
|
||||
initialBridgeStatus:
|
||||
bridgeResult.status === 'fulfilled' ? (bridgeResult.value as MissionControlBridgeStatusResponse) : null,
|
||||
initialRelaySummary:
|
||||
bridgeResult.status === 'fulfilled' ? summarizeMissionControlRelay(bridgeResult.value as never) : null,
|
||||
},
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from '@/utils/search'
|
||||
import PageIntro from '@/components/common/PageIntro'
|
||||
import { fetchPublicJson } from '@/utils/publicExplorer'
|
||||
import { useUiMode } from '@/components/common/UiModeContext'
|
||||
|
||||
type SearchFilterMode = 'all' | 'gru' | 'x402' | 'wrapped'
|
||||
|
||||
@@ -28,6 +29,7 @@ export default function SearchPage({
|
||||
initialRawResults,
|
||||
initialCuratedTokens,
|
||||
}: SearchPageProps) {
|
||||
const { mode } = useUiMode()
|
||||
const router = useRouter()
|
||||
const routerQuery = typeof router.query.q === 'string' ? router.query.q : ''
|
||||
const [query, setQuery] = useState(initialQuery)
|
||||
@@ -193,7 +195,11 @@ export default function SearchPage({
|
||||
<PageIntro
|
||||
eyebrow="Explorer Lookup"
|
||||
title="Search"
|
||||
description="Search by address, transaction hash, block number, or token symbol. Direct identifiers can jump straight into detail pages, while broader terms fall back to indexed search."
|
||||
description={
|
||||
mode === 'guided'
|
||||
? 'Search by address, transaction hash, block number, or token symbol. Direct identifiers can jump straight into detail pages, while broader terms fall back to indexed search.'
|
||||
: 'Search address, tx hash, block, or token symbol. Direct identifiers jump straight to detail pages.'
|
||||
}
|
||||
actions={[
|
||||
{ href: '/tokens', label: 'Token shortcuts' },
|
||||
{ href: '/addresses', label: 'Browse addresses' },
|
||||
@@ -207,7 +213,7 @@ export default function SearchPage({
|
||||
type="text"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search by address, transaction hash, block number..."
|
||||
placeholder={mode === 'guided' ? 'Search by address, transaction hash, block number...' : 'Search tx / addr / block / token'}
|
||||
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
/>
|
||||
<button
|
||||
@@ -237,7 +243,9 @@ export default function SearchPage({
|
||||
{!loading && tokenTarget && (
|
||||
<Card className="mb-6" title="Direct Token Match">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
This matches a curated Chain 138 token, so you can go straight to the token detail page instead of sifting through generic search results.
|
||||
{mode === 'guided'
|
||||
? 'This matches a curated Chain 138 token, so you can go straight to the token detail page instead of sifting through generic search results.'
|
||||
: 'Curated Chain 138 token match.'}
|
||||
</p>
|
||||
<div className="mt-4">
|
||||
<Link href={tokenTarget.href} className="text-primary-600 hover:underline">
|
||||
@@ -250,7 +258,9 @@ export default function SearchPage({
|
||||
{!loading && !tokenTarget && directTarget && (
|
||||
<Card className="mb-6" title="Direct Match">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
This looks like a direct explorer identifier. You can open it without waiting for indexed search results.
|
||||
{mode === 'guided'
|
||||
? 'This looks like a direct explorer identifier. You can open it without waiting for indexed search results.'
|
||||
: 'Direct explorer identifier detected.'}
|
||||
</p>
|
||||
<div className="mt-4">
|
||||
<Link href={directTarget.href} className="text-primary-600 hover:underline">
|
||||
|
||||
@@ -8,9 +8,18 @@ import EntityBadge from '@/components/common/EntityBadge'
|
||||
import PageIntro from '@/components/common/PageIntro'
|
||||
import { fetchPublicJson } from '@/utils/publicExplorer'
|
||||
import { normalizeTransaction } from '@/services/api/blockscout'
|
||||
import { summarizeChainActivity } from '@/utils/activityContext'
|
||||
import ActivityContextPanel from '@/components/common/ActivityContextPanel'
|
||||
import FreshnessTrustNote from '@/components/common/FreshnessTrustNote'
|
||||
import { normalizeExplorerStats, type ExplorerStats } from '@/services/api/stats'
|
||||
import type { MissionControlBridgeStatusResponse } from '@/services/api/missionControl'
|
||||
import { resolveEffectiveFreshness } from '@/utils/explorerFreshness'
|
||||
|
||||
interface TransactionsPageProps {
|
||||
initialTransactions: Transaction[]
|
||||
initialLatestBlocks: Array<{ number: number; timestamp: string }>
|
||||
initialStats: ExplorerStats | null
|
||||
initialBridgeStatus: MissionControlBridgeStatusResponse | null
|
||||
}
|
||||
|
||||
function serializeTransactionList(transactions: Transaction[]): Transaction[] {
|
||||
@@ -33,12 +42,37 @@ function serializeTransactionList(transactions: Transaction[]): Transaction[] {
|
||||
) as Transaction[]
|
||||
}
|
||||
|
||||
export default function TransactionsPage({ initialTransactions }: TransactionsPageProps) {
|
||||
export default function TransactionsPage({
|
||||
initialTransactions,
|
||||
initialLatestBlocks,
|
||||
initialStats,
|
||||
initialBridgeStatus,
|
||||
}: TransactionsPageProps) {
|
||||
const pageSize = 20
|
||||
const [transactions, setTransactions] = useState<Transaction[]>(initialTransactions)
|
||||
const [loading, setLoading] = useState(initialTransactions.length === 0)
|
||||
const [page, setPage] = useState(1)
|
||||
const chainId = parseInt(process.env.NEXT_PUBLIC_CHAIN_ID || '138')
|
||||
const activityContext = useMemo(
|
||||
() =>
|
||||
summarizeChainActivity({
|
||||
blocks: initialLatestBlocks.map((block) => ({
|
||||
chain_id: chainId,
|
||||
number: block.number,
|
||||
hash: '',
|
||||
timestamp: block.timestamp,
|
||||
miner: '',
|
||||
transaction_count: 0,
|
||||
gas_used: 0,
|
||||
gas_limit: 0,
|
||||
})),
|
||||
transactions,
|
||||
latestBlockNumber: initialLatestBlocks[0]?.number ?? null,
|
||||
latestBlockTimestamp: initialLatestBlocks[0]?.timestamp ?? null,
|
||||
freshness: resolveEffectiveFreshness(initialStats, initialBridgeStatus),
|
||||
}),
|
||||
[chainId, initialBridgeStatus, initialLatestBlocks, initialStats, transactions],
|
||||
)
|
||||
|
||||
const loadTransactions = useCallback(async () => {
|
||||
setLoading(true)
|
||||
@@ -163,6 +197,17 @@ export default function TransactionsPage({ initialTransactions }: TransactionsPa
|
||||
]}
|
||||
/>
|
||||
|
||||
<div className="mb-6">
|
||||
<ActivityContextPanel context={activityContext} title="Transaction Recency Context" />
|
||||
<FreshnessTrustNote
|
||||
className="mt-3"
|
||||
context={activityContext}
|
||||
stats={initialStats}
|
||||
bridgeStatus={initialBridgeStatus}
|
||||
scopeLabel="This page reflects the latest indexed visible transaction activity."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!loading && transactions.length > 0 && (
|
||||
<div className="mb-6 grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<Card>
|
||||
@@ -250,14 +295,30 @@ export default function TransactionsPage({ initialTransactions }: TransactionsPa
|
||||
|
||||
export const getServerSideProps: GetServerSideProps<TransactionsPageProps> = async () => {
|
||||
const chainId = Number(process.env.NEXT_PUBLIC_CHAIN_ID || '138')
|
||||
const transactionsResult = await fetchPublicJson<{ items?: unknown[] }>('/api/v2/transactions?page=1&page_size=20').catch(() => null)
|
||||
const [transactionsResult, blocksResult, statsResult, bridgeResult] = await Promise.all([
|
||||
fetchPublicJson<{ items?: unknown[] }>('/api/v2/transactions?page=1&page_size=20').catch(() => null),
|
||||
fetchPublicJson<{ items?: Array<{ height?: number | string | null; timestamp?: string | null }> }>('/api/v2/blocks?page=1&page_size=3').catch(() => null),
|
||||
fetchPublicJson<Record<string, unknown>>('/api/v2/stats').catch(() => null),
|
||||
fetchPublicJson<MissionControlBridgeStatusResponse>('/explorer-api/v1/track1/bridge/status').catch(() => null),
|
||||
])
|
||||
const initialTransactions = Array.isArray(transactionsResult?.items)
|
||||
? transactionsResult.items.map((item) => normalizeTransaction(item as never, chainId))
|
||||
: []
|
||||
const initialLatestBlocks = Array.isArray(blocksResult?.items)
|
||||
? blocksResult.items
|
||||
.map((item) => ({
|
||||
number: Number(item.height || 0),
|
||||
timestamp: item.timestamp || '',
|
||||
}))
|
||||
.filter((item) => Number.isFinite(item.number) && item.number > 0 && item.timestamp)
|
||||
: []
|
||||
|
||||
return {
|
||||
props: {
|
||||
initialTransactions: serializeTransactionList(initialTransactions),
|
||||
initialLatestBlocks,
|
||||
initialStats: statsResult ? normalizeExplorerStats(statsResult as never) : null,
|
||||
initialBridgeStatus: bridgeResult,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user