refactor: rename SolaceScanScout to Solace and update related configurations

- Updated branding from "SolaceScanScout" to "Solace" across various files including deployment scripts, API responses, and documentation.
- Changed default base URL for Playwright tests and updated security headers to reflect the new branding.
- Enhanced README and API documentation to include new authentication endpoints and product access details.

This refactor aligns the project branding and improves clarity in the API documentation.
This commit is contained in:
defiQUG
2026-04-10 12:52:17 -07:00
parent 6eef6b07f6
commit 0972178cc5
160 changed files with 13274 additions and 1061 deletions

View File

@@ -1,5 +1,6 @@
import { useEffect, useMemo, useState } from 'react'
import { Card } from '@/libs/frontend-ui-primitives'
import Link from 'next/link'
import { explorerFeaturePages } from '@/data/explorerOperations'
import { blocksApi, type Block } from '@/services/api/blocks'
import {
@@ -7,8 +8,14 @@ import {
type MissionControlBridgeStatusResponse,
type MissionControlChainStatus,
} from '@/services/api/missionControl'
import { statsApi, type ExplorerStats } from '@/services/api/stats'
import {
statsApi,
type ExplorerRecentActivitySnapshot,
type ExplorerStats,
type ExplorerTransactionTrendPoint,
} from '@/services/api/stats'
import { transactionsApi, type Transaction } from '@/services/api/transactions'
import { formatWeiAsEth } from '@/utils/format'
import OperationsPageShell, {
MetricCard,
StatusBadge,
@@ -17,6 +24,15 @@ import OperationsPageShell, {
truncateMiddle,
} from './OperationsPageShell'
interface AnalyticsOperationsPageProps {
initialStats?: ExplorerStats | null
initialTransactionTrend?: ExplorerTransactionTrendPoint[]
initialActivitySnapshot?: ExplorerRecentActivitySnapshot | null
initialBlocks?: Block[]
initialTransactions?: Transaction[]
initialBridgeStatus?: MissionControlBridgeStatusResponse | null
}
function getChainStatus(bridgeStatus: MissionControlBridgeStatusResponse | null): MissionControlChainStatus | null {
const chains = bridgeStatus?.data?.chains
if (!chains) return null
@@ -24,11 +40,20 @@ function getChainStatus(bridgeStatus: MissionControlBridgeStatusResponse | null)
return firstChain || null
}
export default function AnalyticsOperationsPage() {
const [stats, setStats] = useState<ExplorerStats | null>(null)
const [blocks, setBlocks] = useState<Block[]>([])
const [transactions, setTransactions] = useState<Transaction[]>([])
const [bridgeStatus, setBridgeStatus] = useState<MissionControlBridgeStatusResponse | null>(null)
export default function AnalyticsOperationsPage({
initialStats = null,
initialTransactionTrend = [],
initialActivitySnapshot = null,
initialBlocks = [],
initialTransactions = [],
initialBridgeStatus = null,
}: AnalyticsOperationsPageProps) {
const [stats, setStats] = useState<ExplorerStats | null>(initialStats)
const [transactionTrend, setTransactionTrend] = useState<ExplorerTransactionTrendPoint[]>(initialTransactionTrend)
const [activitySnapshot, setActivitySnapshot] = useState<ExplorerRecentActivitySnapshot | null>(initialActivitySnapshot)
const [blocks, setBlocks] = useState<Block[]>(initialBlocks)
const [transactions, setTransactions] = useState<Transaction[]>(initialTransactions)
const [bridgeStatus, setBridgeStatus] = useState<MissionControlBridgeStatusResponse | null>(initialBridgeStatus)
const [loadingError, setLoadingError] = useState<string | null>(null)
const page = explorerFeaturePages.analytics
@@ -36,8 +61,10 @@ export default function AnalyticsOperationsPage() {
let cancelled = false
const load = async () => {
const [statsResult, blocksResult, transactionsResult, bridgeResult] = await Promise.allSettled([
const [statsResult, trendResult, snapshotResult, blocksResult, transactionsResult, bridgeResult] = await Promise.allSettled([
statsApi.get(),
statsApi.getTransactionTrend(),
statsApi.getRecentActivitySnapshot(),
blocksApi.list({ chain_id: 138, page: 1, page_size: 5 }),
transactionsApi.list(138, 1, 5),
missionControlApi.getBridgeStatus(),
@@ -46,15 +73,17 @@ export default function AnalyticsOperationsPage() {
if (cancelled) return
if (statsResult.status === 'fulfilled') setStats(statsResult.value)
if (trendResult.status === 'fulfilled') setTransactionTrend(trendResult.value)
if (snapshotResult.status === 'fulfilled') setActivitySnapshot(snapshotResult.value)
if (blocksResult.status === 'fulfilled') setBlocks(blocksResult.value.data)
if (transactionsResult.status === 'fulfilled') setTransactions(transactionsResult.value.data)
if (bridgeResult.status === 'fulfilled') setBridgeStatus(bridgeResult.value)
const failedCount = [statsResult, blocksResult, transactionsResult, bridgeResult].filter(
const failedCount = [statsResult, trendResult, snapshotResult, blocksResult, transactionsResult, bridgeResult].filter(
(result) => result.status === 'rejected'
).length
if (failedCount === 4) {
if (failedCount === 6) {
setLoadingError('Analytics data is temporarily unavailable from the public explorer APIs.')
}
}
@@ -71,6 +100,27 @@ export default function AnalyticsOperationsPage() {
}, [])
const chainStatus = useMemo(() => getChainStatus(bridgeStatus), [bridgeStatus])
const trailingWindow = useMemo(() => transactionTrend.slice(0, 7), [transactionTrend])
const sevenDayAverage = useMemo(() => {
if (trailingWindow.length === 0) return 0
const total = trailingWindow.reduce((sum, point) => sum + point.transaction_count, 0)
return total / trailingWindow.length
}, [trailingWindow])
const topDay = useMemo(() => {
if (trailingWindow.length === 0) return null
return trailingWindow.reduce((best, point) => (point.transaction_count > best.transaction_count ? point : best))
}, [trailingWindow])
const averageGasUtilization = useMemo(() => {
if (blocks.length === 0) return 0
return blocks.reduce((sum, block) => {
const ratio = block.gas_limit > 0 ? block.gas_used / block.gas_limit : 0
return sum + ratio
}, 0) / blocks.length
}, [blocks])
const trendPeak = useMemo(
() => trailingWindow.reduce((max, point) => Math.max(max, point.transaction_count), 0),
[trailingWindow],
)
return (
<OperationsPageShell page={page}>
@@ -107,9 +157,111 @@ export default function AnalyticsOperationsPage() {
: 'Latest public RPC head age from mission control.'
}
/>
<MetricCard
title="7d Avg Tx"
value={formatNumber(Math.round(sevenDayAverage))}
description="Average daily transactions over the latest seven charted days."
className="border border-violet-200 bg-violet-50/70 dark:border-violet-900/50 dark:bg-violet-950/20"
/>
<MetricCard
title="Recent Success Rate"
value={activitySnapshot ? `${Math.round(activitySnapshot.success_rate * 100)}%` : 'Unknown'}
description="Success rate across the public main-page transaction sample."
/>
<MetricCard
title="Failure Rate"
value={activitySnapshot ? `${Math.round(activitySnapshot.failure_rate * 100)}%` : 'Unknown'}
description="The complement to the recent success rate in the visible sample."
className="border border-rose-200 bg-rose-50/70 dark:border-rose-900/50 dark:bg-rose-950/20"
/>
<MetricCard
title="Avg Gas Used"
value={activitySnapshot ? formatNumber(Math.round(activitySnapshot.average_gas_used)) : 'Unknown'}
description="Average gas used in the recent sampled transactions."
/>
<MetricCard
title="Avg Block Gas"
value={`${Math.round(averageGasUtilization * 100)}%`}
description="Average gas utilization across the latest visible blocks."
className="border border-amber-200 bg-amber-50/70 dark:border-amber-900/50 dark:bg-amber-950/20"
/>
</div>
<div className="mb-8 grid gap-6 lg:grid-cols-[1fr_1fr]">
<Card title="Activity Trend">
<div className="space-y-4">
<div className="grid gap-4 sm:grid-cols-3">
<div className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40">
<div className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Peak Day</div>
<div className="mt-2 text-xl font-semibold text-gray-900 dark:text-white">
{topDay ? formatNumber(topDay.transaction_count) : 'Unknown'}
</div>
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">{topDay?.date || 'No trend data yet'}</div>
</div>
<div className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40">
<div className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Contract Creations</div>
<div className="mt-2 text-xl font-semibold text-gray-900 dark:text-white">
{formatNumber(activitySnapshot?.contract_creations)}
</div>
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">Within the sampled recent transaction feed.</div>
</div>
<div className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40">
<div className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Avg Sample Fee</div>
<div className="mt-2 text-xl font-semibold text-gray-900 dark:text-white">
{activitySnapshot ? formatWeiAsEth(Math.round(activitySnapshot.average_fee_wei).toString(), 6) : 'Unknown'}
</div>
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">Average fee from the recent public transaction sample.</div>
</div>
</div>
{activitySnapshot ? (
<div className="grid gap-3 sm:grid-cols-3">
<div className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40">
<div className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Token Transfer Share</div>
<div className="mt-2 text-xl font-semibold text-gray-900 dark:text-white">
{Math.round(activitySnapshot.token_transfer_share * 100)}%
</div>
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">Sampled transactions involving token transfers.</div>
</div>
<div className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40">
<div className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Contract Call Share</div>
<div className="mt-2 text-xl font-semibold text-gray-900 dark:text-white">
{Math.round(activitySnapshot.contract_call_share * 100)}%
</div>
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">Sampled transactions calling contracts.</div>
</div>
<div className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40">
<div className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Creation Share</div>
<div className="mt-2 text-xl font-semibold text-gray-900 dark:text-white">
{Math.round(activitySnapshot.contract_creation_share * 100)}%
</div>
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">Sampled transactions deploying contracts.</div>
</div>
</div>
) : null}
<div className="space-y-3">
{trailingWindow.map((point) => {
const width = trendPeak > 0 ? Math.max(8, Math.round((point.transaction_count / trendPeak) * 100)) : 0
return (
<div key={point.date}>
<div className="mb-1 flex items-center justify-between text-sm text-gray-600 dark:text-gray-400">
<span>{point.date}</span>
<span>{formatNumber(point.transaction_count)} tx</span>
</div>
<div className="h-2 rounded-full bg-gray-200 dark:bg-gray-800">
<div className="h-2 rounded-full bg-primary-600" style={{ width: `${width}%` }} />
</div>
</div>
)
})}
{trailingWindow.length === 0 ? (
<p className="text-sm text-gray-600 dark:text-gray-400">Trend data is temporarily unavailable.</p>
) : null}
</div>
</div>
</Card>
<Card title="Recent Blocks">
<div className="space-y-4">
{blocks.map((block) => (
@@ -119,15 +271,18 @@ export default function AnalyticsOperationsPage() {
>
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div>
<div className="text-base font-semibold text-gray-900 dark:text-white">
<Link href={`/blocks/${block.number}`} className="text-base font-semibold text-primary-600 hover:underline">
Block {formatNumber(block.number)}
</div>
</Link>
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{truncateMiddle(block.hash)} · miner {truncateMiddle(block.miner)}
{truncateMiddle(block.hash)} · miner{' '}
<Link href={`/addresses/${block.miner}`} className="text-primary-600 hover:underline">
{truncateMiddle(block.miner)}
</Link>
</div>
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">
{formatNumber(block.transaction_count)} tx · {relativeAge(block.timestamp)}
{formatNumber(block.transaction_count)} tx · {Math.round((block.gas_limit > 0 ? block.gas_used / block.gas_limit : 0) * 100)}% gas · {relativeAge(block.timestamp)}
</div>
</div>
</div>
@@ -147,11 +302,26 @@ export default function AnalyticsOperationsPage() {
>
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div>
<div className="text-base font-semibold text-gray-900 dark:text-white">
<Link href={`/transactions/${transaction.hash}`} className="text-base font-semibold text-primary-600 hover:underline">
{truncateMiddle(transaction.hash, 12, 10)}
</div>
</Link>
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
Block {formatNumber(transaction.block_number)} · from {truncateMiddle(transaction.from_address)}
Block{' '}
<Link href={`/blocks/${transaction.block_number}`} className="text-primary-600 hover:underline">
{formatNumber(transaction.block_number)}
</Link>
{' '}· from{' '}
<Link href={`/addresses/${transaction.from_address}`} className="text-primary-600 hover:underline">
{truncateMiddle(transaction.from_address)}
</Link>
{transaction.to_address ? (
<>
{' '}· to{' '}
<Link href={`/addresses/${transaction.to_address}`} className="text-primary-600 hover:underline">
{truncateMiddle(transaction.to_address)}
</Link>
</>
) : null}
</div>
</div>
<div className="flex items-center gap-3">
@@ -164,6 +334,13 @@ export default function AnalyticsOperationsPage() {
</div>
</div>
</div>
<div className="mt-3 flex flex-wrap gap-2 text-xs">
{transaction.method ? <StatusBadge status={transaction.method} tone="warning" /> : null}
{transaction.contract_address ? <StatusBadge status="contract creation" tone="warning" /> : null}
{transaction.token_transfers && transaction.token_transfers.length > 0 ? (
<StatusBadge status={`${transaction.token_transfers.length} token transfer${transaction.token_transfers.length === 1 ? '' : 's'}`} />
) : null}
</div>
</div>
))}
{transactions.length === 0 ? (