Improve explorer subsystem posture and wallet visibility
This commit is contained in:
@@ -52,6 +52,11 @@ function buildDetail(context: ChainActivityContext, diagnosticExplanation?: stri
|
||||
return `Latest visible transaction: ${latestTxAge}. Recent head blocks may be quiet even while the chain remains current.`
|
||||
}
|
||||
|
||||
function normalizeSentence(value?: string | null): string {
|
||||
if (!value) return ''
|
||||
return value.trim().replace(/[.\s]+$/, '')
|
||||
}
|
||||
|
||||
export default function FreshnessTrustNote({
|
||||
context,
|
||||
stats,
|
||||
@@ -96,7 +101,9 @@ export default function FreshnessTrustNote({
|
||||
<div className={`rounded-2xl border border-gray-200 bg-white/80 px-4 py-3 text-sm dark:border-gray-800 dark:bg-gray-950/40${normalizedClassName}`}>
|
||||
<div className="font-medium text-gray-900 dark:text-white">{buildSummary(context)}</div>
|
||||
<div className="mt-1 text-gray-600 dark:text-gray-400">
|
||||
{buildDetail(context, diagnosticExplanation)} {scopeLabel ? `${scopeLabel}. ` : ''}{sourceLabel}
|
||||
{normalizeSentence(buildDetail(context, diagnosticExplanation))}.{' '}
|
||||
{scopeLabel ? `${normalizeSentence(scopeLabel)}. ` : ''}
|
||||
{normalizeSentence(sourceLabel)}.
|
||||
</div>
|
||||
<div className="mt-2 flex flex-wrap gap-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
{confidenceBadges.map((badge) => (
|
||||
|
||||
127
frontend/src/components/common/SubsystemPosturePanel.tsx
Normal file
127
frontend/src/components/common/SubsystemPosturePanel.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import { Card } from '@/libs/frontend-ui-primitives'
|
||||
import { useUiMode } from './UiModeContext'
|
||||
import type { MissionControlSubsystemStatus } from '@/services/api/missionControl'
|
||||
import { formatRelativeAge } from '@/utils/format'
|
||||
|
||||
function subsystemLabel(key: string): string {
|
||||
const labels: Record<string, string> = {
|
||||
rpc_head: 'RPC head',
|
||||
tx_index: 'Transaction index',
|
||||
stats_summary: 'Stats summary',
|
||||
bridge_relay_monitoring: 'Bridge relay monitoring',
|
||||
freshness_queries: 'Freshness queries',
|
||||
}
|
||||
|
||||
return labels[key] || key.replace(/_/g, ' ')
|
||||
}
|
||||
|
||||
function normalizeStatus(status?: string | null): string {
|
||||
const normalized = String(status || '').toLowerCase()
|
||||
if (!normalized) return 'unknown'
|
||||
if (normalized === 'ok') return 'operational'
|
||||
return normalized
|
||||
}
|
||||
|
||||
function statusClasses(status?: string | null): string {
|
||||
const normalized = normalizeStatus(status)
|
||||
if (['degraded', 'down', 'stale'].includes(normalized)) {
|
||||
return 'border-red-200 bg-red-50 text-red-700 dark:border-red-900/60 dark:bg-red-950/30 dark:text-red-200'
|
||||
}
|
||||
if (['warning', 'partial', 'paused'].includes(normalized)) {
|
||||
return 'border-amber-200 bg-amber-50 text-amber-800 dark:border-amber-900/60 dark:bg-amber-950/30 dark:text-amber-200'
|
||||
}
|
||||
if (normalized === 'operational') {
|
||||
return 'border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-900/60 dark:bg-emerald-950/30 dark:text-emerald-200'
|
||||
}
|
||||
return 'border-gray-200 bg-gray-50 text-gray-700 dark:border-gray-700 dark:bg-gray-900/50 dark:text-gray-200'
|
||||
}
|
||||
|
||||
function compactMeta(subsystem: MissionControlSubsystemStatus): string {
|
||||
const parts = [
|
||||
subsystem.source || null,
|
||||
subsystem.completeness || null,
|
||||
subsystem.confidence || null,
|
||||
].filter(Boolean)
|
||||
|
||||
return parts.length > 0 ? parts.join(' · ') : 'No supporting freshness metadata'
|
||||
}
|
||||
|
||||
export default function SubsystemPosturePanel({
|
||||
subsystems,
|
||||
title = 'Subsystem Posture',
|
||||
scopeLabel,
|
||||
preferredKeys,
|
||||
className = '',
|
||||
}: {
|
||||
subsystems?: Record<string, MissionControlSubsystemStatus> | null
|
||||
title?: string
|
||||
scopeLabel?: string
|
||||
preferredKeys?: string[]
|
||||
className?: string
|
||||
}) {
|
||||
const { mode } = useUiMode()
|
||||
|
||||
const orderedEntries = Object.entries(subsystems || {})
|
||||
.filter(([key]) => !preferredKeys || preferredKeys.includes(key))
|
||||
.sort(([leftKey], [rightKey]) => {
|
||||
const leftIndex = preferredKeys ? preferredKeys.indexOf(leftKey) : -1
|
||||
const rightIndex = preferredKeys ? preferredKeys.indexOf(rightKey) : -1
|
||||
if (leftIndex !== -1 || rightIndex !== -1) {
|
||||
return (leftIndex === -1 ? Number.MAX_SAFE_INTEGER : leftIndex) - (rightIndex === -1 ? Number.MAX_SAFE_INTEGER : rightIndex)
|
||||
}
|
||||
return leftKey.localeCompare(rightKey)
|
||||
})
|
||||
|
||||
if (orderedEntries.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const normalizedClassName = className ? ` ${className}` : ''
|
||||
|
||||
return (
|
||||
<Card className={`border border-gray-200 bg-white/80 dark:border-gray-800 dark:bg-gray-950/40${normalizedClassName}`} title={title}>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="text-sm leading-6 text-gray-600 dark:text-gray-400">
|
||||
{scopeLabel || 'These subsystem signals come from the same backend freshness model used to explain chain and transaction visibility.'}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
|
||||
{orderedEntries.map(([key, subsystem]) => {
|
||||
const status = normalizeStatus(subsystem.status)
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className="rounded-2xl border border-gray-200 bg-gray-50/70 p-4 dark:border-gray-800 dark:bg-gray-900/40"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
{subsystemLabel(key)}
|
||||
</div>
|
||||
<div className="mt-1 text-base font-semibold capitalize text-gray-900 dark:text-white">
|
||||
{status}
|
||||
</div>
|
||||
</div>
|
||||
<span className={`rounded-full border px-2.5 py-1 text-[11px] font-semibold uppercase tracking-wide ${statusClasses(status)}`}>
|
||||
{status}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
{subsystem.updated_at ? `Updated ${formatRelativeAge(subsystem.updated_at)}` : 'Update time unavailable'}
|
||||
</div>
|
||||
<div className="mt-2 text-xs leading-5 text-gray-500 dark:text-gray-400">
|
||||
{compactMeta(subsystem)}
|
||||
</div>
|
||||
{mode === 'guided' && subsystem.provenance ? (
|
||||
<div className="mt-2 text-xs leading-5 text-gray-500 dark:text-gray-400">
|
||||
Provenance: {subsystem.provenance.replace(/_/g, ' ')}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import { formatWeiAsEth } from '@/utils/format'
|
||||
import { summarizeChainActivity } from '@/utils/activityContext'
|
||||
import ActivityContextPanel from '@/components/common/ActivityContextPanel'
|
||||
import FreshnessTrustNote from '@/components/common/FreshnessTrustNote'
|
||||
import SubsystemPosturePanel from '@/components/common/SubsystemPosturePanel'
|
||||
import { resolveEffectiveFreshness, shouldExplainEmptyHeadBlocks } from '@/utils/explorerFreshness'
|
||||
import OperationsPageShell, {
|
||||
MetricCard,
|
||||
@@ -155,6 +156,13 @@ export default function AnalyticsOperationsPage({
|
||||
bridgeStatus={bridgeStatus}
|
||||
scopeLabel="This page combines public stats, recent block samples, and indexed transactions."
|
||||
/>
|
||||
<SubsystemPosturePanel
|
||||
className="mt-3"
|
||||
subsystems={bridgeStatus?.data?.subsystems}
|
||||
title="Analytics Subsystem Posture"
|
||||
preferredKeys={['rpc_head', 'tx_index', 'stats_summary', 'freshness_queries']}
|
||||
scopeLabel="These subsystem signals explain whether sparse analytics reflect quiet-chain conditions, partial transaction indexing, or stale summary generation."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-6 grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
|
||||
@@ -14,6 +14,7 @@ import { explorerFeaturePages } from '@/data/explorerOperations'
|
||||
import { summarizeChainActivity } from '@/utils/activityContext'
|
||||
import ActivityContextPanel from '@/components/common/ActivityContextPanel'
|
||||
import FreshnessTrustNote from '@/components/common/FreshnessTrustNote'
|
||||
import SubsystemPosturePanel from '@/components/common/SubsystemPosturePanel'
|
||||
import { resolveEffectiveFreshness } from '@/utils/explorerFreshness'
|
||||
|
||||
type FeedState = 'connecting' | 'live' | 'fallback'
|
||||
@@ -288,6 +289,13 @@ export default function BridgeMonitoringPage({
|
||||
bridgeStatus={bridgeStatus}
|
||||
scopeLabel="Bridge relay posture is shown alongside the same explorer freshness model used on the homepage and core explorer routes"
|
||||
/>
|
||||
<SubsystemPosturePanel
|
||||
className="mt-3"
|
||||
subsystems={bridgeStatus?.data?.subsystems}
|
||||
title="Bridge Subsystem Posture"
|
||||
preferredKeys={['rpc_head', 'tx_index', 'bridge_relay_monitoring', 'stats_summary', 'freshness_queries']}
|
||||
scopeLabel="These bridge-facing subsystem signals show whether the limiting factor is public head visibility, transaction indexing, relay monitoring, or degraded freshness queries."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-6 grid gap-4 lg:grid-cols-3">
|
||||
|
||||
@@ -20,6 +20,7 @@ import { statsApi, type ExplorerStats } from '@/services/api/stats'
|
||||
import { summarizeChainActivity } from '@/utils/activityContext'
|
||||
import ActivityContextPanel from '@/components/common/ActivityContextPanel'
|
||||
import FreshnessTrustNote from '@/components/common/FreshnessTrustNote'
|
||||
import SubsystemPosturePanel from '@/components/common/SubsystemPosturePanel'
|
||||
import { resolveEffectiveFreshness } from '@/utils/explorerFreshness'
|
||||
import {
|
||||
formatCurrency,
|
||||
@@ -277,6 +278,13 @@ export default function LiquidityOperationsPage({
|
||||
bridgeStatus={bridgeStatus}
|
||||
scopeLabel="Liquidity inventory and planner posture are shown alongside the same explorer freshness model used on the homepage and core explorer routes"
|
||||
/>
|
||||
<SubsystemPosturePanel
|
||||
className="mt-3"
|
||||
subsystems={bridgeStatus?.data?.subsystems}
|
||||
title="Liquidity Subsystem Posture"
|
||||
preferredKeys={['rpc_head', 'tx_index', 'stats_summary', 'freshness_queries']}
|
||||
scopeLabel="Use these subsystem signals to tell whether sparse liquidity visibility is consistent with quiet-chain conditions, partial transaction indexing, or stale public summary data."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-8 grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useUiMode } from '@/components/common/UiModeContext'
|
||||
import { summarizeChainActivity } from '@/utils/activityContext'
|
||||
import ActivityContextPanel from '@/components/common/ActivityContextPanel'
|
||||
import FreshnessTrustNote from '@/components/common/FreshnessTrustNote'
|
||||
import SubsystemPosturePanel from '@/components/common/SubsystemPosturePanel'
|
||||
import { resolveEffectiveFreshness } from '@/utils/explorerFreshness'
|
||||
import { statsApi, type ExplorerStats } from '@/services/api/stats'
|
||||
|
||||
@@ -203,6 +204,13 @@ export default function OperationsHubPage({
|
||||
bridgeStatus={bridgeStatus}
|
||||
scopeLabel="This page reflects mission-control freshness, public bridge status, and explorer-served config surfaces."
|
||||
/>
|
||||
<SubsystemPosturePanel
|
||||
className="mt-3"
|
||||
subsystems={bridgeStatus?.data?.subsystems}
|
||||
title="Operations Subsystem Posture"
|
||||
preferredKeys={['rpc_head', 'tx_index', 'bridge_relay_monitoring', 'stats_summary', 'freshness_queries']}
|
||||
scopeLabel="Operations posture is grounded in the same backend subsystem truth used for chain activity, route inventory, and bridge monitoring."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-6 grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
|
||||
@@ -14,6 +14,7 @@ import { statsApi, type ExplorerStats } from '@/services/api/stats'
|
||||
import { summarizeChainActivity } from '@/utils/activityContext'
|
||||
import ActivityContextPanel from '@/components/common/ActivityContextPanel'
|
||||
import FreshnessTrustNote from '@/components/common/FreshnessTrustNote'
|
||||
import SubsystemPosturePanel from '@/components/common/SubsystemPosturePanel'
|
||||
import { resolveEffectiveFreshness } from '@/utils/explorerFreshness'
|
||||
|
||||
interface RoutesMonitoringPageProps {
|
||||
@@ -238,6 +239,13 @@ export default function RoutesMonitoringPage({
|
||||
bridgeStatus={bridgeStatus}
|
||||
scopeLabel="Route availability reflects the current public route matrix and the same explorer freshness model used on the core explorer pages"
|
||||
/>
|
||||
<SubsystemPosturePanel
|
||||
className="mt-3"
|
||||
subsystems={bridgeStatus?.data?.subsystems}
|
||||
title="Route Subsystem Posture"
|
||||
preferredKeys={['rpc_head', 'tx_index', 'stats_summary', 'freshness_queries']}
|
||||
scopeLabel="Route inventory is read against the same backend freshness signals used to explain whether route posture is limited by head visibility, transaction indexing, or stale summary data."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-6 grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
|
||||
@@ -10,7 +10,14 @@ import Link from 'next/link'
|
||||
import { Explain, useUiMode } from '@/components/common/UiModeContext'
|
||||
import { accessApi, type WalletAccessSession } from '@/services/api/access'
|
||||
import EntityBadge from '@/components/common/EntityBadge'
|
||||
import { addressesApi, type AddressInfo, type TransactionSummary } from '@/services/api/addresses'
|
||||
import {
|
||||
addressesApi,
|
||||
type AddressInfo,
|
||||
type AddressTokenBalance,
|
||||
type AddressTokenTransfer,
|
||||
type TransactionSummary,
|
||||
} from '@/services/api/addresses'
|
||||
import { formatRelativeAge, formatTokenAmount } from '@/utils/format'
|
||||
import {
|
||||
isWatchlistEntry,
|
||||
readWatchlistFromStorage,
|
||||
@@ -42,6 +49,8 @@ export default function WalletPage(props: WalletPageProps) {
|
||||
const [watchlistEntries, setWatchlistEntries] = useState<string[]>([])
|
||||
const [addressInfo, setAddressInfo] = useState<AddressInfo | null>(null)
|
||||
const [recentAddressTransactions, setRecentAddressTransactions] = useState<TransactionSummary[]>([])
|
||||
const [tokenBalances, setTokenBalances] = useState<AddressTokenBalance[]>([])
|
||||
const [tokenTransfers, setTokenTransfers] = useState<AddressTokenTransfer[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return
|
||||
@@ -107,6 +116,8 @@ export default function WalletPage(props: WalletPageProps) {
|
||||
if (!walletSession?.address) {
|
||||
setAddressInfo(null)
|
||||
setRecentAddressTransactions([])
|
||||
setTokenBalances([])
|
||||
setTokenTransfers([])
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
@@ -115,16 +126,41 @@ export default function WalletPage(props: WalletPageProps) {
|
||||
Promise.all([
|
||||
addressesApi.getSafe(138, walletSession.address),
|
||||
addressesApi.getTransactionsSafe(138, walletSession.address, 1, 3),
|
||||
addressesApi.getTokenBalancesSafe(walletSession.address),
|
||||
addressesApi.getTokenTransfersSafe(walletSession.address, 1, 4),
|
||||
])
|
||||
.then(([infoResponse, transactionsResponse]) => {
|
||||
.then(([infoResponse, transactionsResponse, balancesResponse, transfersResponse]) => {
|
||||
if (cancelled) return
|
||||
setAddressInfo(infoResponse.ok ? infoResponse.data : null)
|
||||
setRecentAddressTransactions(transactionsResponse.ok ? transactionsResponse.data : [])
|
||||
setTokenBalances(
|
||||
balancesResponse.ok
|
||||
? [...balancesResponse.data]
|
||||
.filter((balance) => {
|
||||
try {
|
||||
return BigInt(balance.value || '0') > 0n
|
||||
} catch {
|
||||
return Boolean(balance.value)
|
||||
}
|
||||
})
|
||||
.sort((left, right) => {
|
||||
try {
|
||||
return Number(BigInt(right.value || '0') - BigInt(left.value || '0'))
|
||||
} catch {
|
||||
return 0
|
||||
}
|
||||
})
|
||||
.slice(0, 4)
|
||||
: [],
|
||||
)
|
||||
setTokenTransfers(transfersResponse.ok ? transfersResponse.data : [])
|
||||
})
|
||||
.catch(() => {
|
||||
if (cancelled) return
|
||||
setAddressInfo(null)
|
||||
setRecentAddressTransactions([])
|
||||
setTokenBalances([])
|
||||
setTokenTransfers([])
|
||||
})
|
||||
|
||||
return () => {
|
||||
@@ -320,6 +356,108 @@ export default function WalletPage(props: WalletPageProps) {
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 grid gap-4 xl:grid-cols-[0.9fr_1.1fr]">
|
||||
<div className="rounded-2xl border border-gray-200 bg-gray-50/70 p-4 dark:border-gray-800 dark:bg-gray-900/40">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
Visible Token Balances
|
||||
</div>
|
||||
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
{mode === 'guided'
|
||||
? 'These are the first visible non-zero token balances currently indexed for your connected wallet.'
|
||||
: 'Indexed non-zero token balances for this wallet.'}
|
||||
</div>
|
||||
</div>
|
||||
<Link href={`/addresses/${walletSession.address}`} className="text-sm font-medium text-primary-600 hover:underline">
|
||||
Full address detail →
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 space-y-3">
|
||||
{tokenBalances.length === 0 ? (
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
No indexed token balances are currently visible for this wallet.
|
||||
</div>
|
||||
) : (
|
||||
tokenBalances.map((balance) => (
|
||||
<Link
|
||||
key={balance.token_address}
|
||||
href={`/tokens/${balance.token_address}`}
|
||||
className="block rounded-xl border border-gray-200 bg-white/80 px-4 py-3 hover:border-primary-300 hover:bg-primary-50/60 dark:border-gray-700 dark:bg-black/10 dark:hover:border-primary-700 dark:hover:bg-primary-950/20"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div className="font-semibold text-gray-900 dark:text-white">
|
||||
{balance.token_symbol || balance.token_name || 'Token'}
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{balance.token_type || 'token'} · {balance.token_name || balance.token_address}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right text-sm font-medium text-gray-900 dark:text-white">
|
||||
{formatTokenAmount(balance.value, balance.token_decimals, balance.token_symbol, 6)}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-gray-200 bg-gray-50/70 p-4 dark:border-gray-800 dark:bg-gray-900/40">
|
||||
<div className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
Recent Token Transfers
|
||||
</div>
|
||||
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
{mode === 'guided'
|
||||
? 'Use these token transfers to jump directly into recent visible asset movement for the connected wallet.'
|
||||
: 'Recent indexed token transfer activity for this wallet.'}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 space-y-3">
|
||||
{tokenTransfers.length === 0 ? (
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
No recent token transfers are currently visible for this connected wallet.
|
||||
</div>
|
||||
) : (
|
||||
tokenTransfers.map((transfer) => {
|
||||
const incoming = transfer.to_address.toLowerCase() === walletSession.address.toLowerCase()
|
||||
const counterparty = incoming ? transfer.from_address : transfer.to_address
|
||||
return (
|
||||
<div
|
||||
key={`${transfer.transaction_hash}-${transfer.token_address}`}
|
||||
className="rounded-xl border border-gray-200 bg-white/80 px-4 py-3 dark:border-gray-700 dark:bg-black/10"
|
||||
>
|
||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div>
|
||||
<div className="font-semibold text-gray-900 dark:text-white">
|
||||
{incoming ? 'Incoming' : 'Outgoing'} {transfer.token_symbol || 'token'} transfer
|
||||
</div>
|
||||
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
{formatTokenAmount(transfer.value, transfer.token_decimals, transfer.token_symbol, 6)}
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
Counterparty: {counterparty.slice(0, 6)}...{counterparty.slice(-4)} · {formatRelativeAge(transfer.timestamp)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-3 text-sm">
|
||||
<Link href={`/transactions/${transfer.transaction_hash}`} className="text-primary-600 hover:underline">
|
||||
Open tx →
|
||||
</Link>
|
||||
<Link href={`/addresses/${counterparty}`} className="text-primary-600 hover:underline">
|
||||
Counterparty →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user