- 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.
191 lines
6.5 KiB
TypeScript
191 lines
6.5 KiB
TypeScript
import type { GetServerSideProps } from 'next'
|
|
import Link from 'next/link'
|
|
import { useEffect, useMemo, useState } from 'react'
|
|
import { useRouter } from 'next/router'
|
|
import { Card, Address } from '@/libs/frontend-ui-primitives'
|
|
import { transactionsApi, Transaction } from '@/services/api/transactions'
|
|
import { readWatchlistFromStorage } from '@/utils/watchlist'
|
|
import PageIntro from '@/components/common/PageIntro'
|
|
import { fetchPublicJson } from '@/utils/publicExplorer'
|
|
import { normalizeTransaction } from '@/services/api/blockscout'
|
|
|
|
function normalizeAddress(value: string) {
|
|
const trimmed = value.trim()
|
|
return /^0x[a-fA-F0-9]{40}$/.test(trimmed) ? trimmed : ''
|
|
}
|
|
|
|
interface AddressesPageProps {
|
|
initialRecentTransactions: Transaction[]
|
|
}
|
|
|
|
function serializeRecentTransactions(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,
|
|
})),
|
|
),
|
|
) as Transaction[]
|
|
}
|
|
|
|
export default function AddressesPage({ initialRecentTransactions }: 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[]>([])
|
|
|
|
useEffect(() => {
|
|
if (initialRecentTransactions.length > 0) {
|
|
setRecentTransactions(initialRecentTransactions)
|
|
return
|
|
}
|
|
|
|
let active = true
|
|
transactionsApi.listSafe(chainId, 1, 20)
|
|
.then(({ ok, data }) => {
|
|
if (active && ok) {
|
|
setRecentTransactions(data)
|
|
}
|
|
})
|
|
.catch(() => {
|
|
if (active) {
|
|
setRecentTransactions([])
|
|
}
|
|
})
|
|
return () => {
|
|
active = false
|
|
}
|
|
}, [chainId, initialRecentTransactions])
|
|
|
|
useEffect(() => {
|
|
if (typeof window === 'undefined') {
|
|
return
|
|
}
|
|
|
|
try {
|
|
setWatchlist(readWatchlistFromStorage(window.localStorage))
|
|
} catch {
|
|
setWatchlist([])
|
|
}
|
|
}, [])
|
|
|
|
const activeAddresses = useMemo(() => {
|
|
const seen = new Set<string>()
|
|
const addresses: string[] = []
|
|
|
|
for (const tx of recentTransactions) {
|
|
for (const candidate of [tx.from_address, tx.to_address]) {
|
|
if (!candidate) continue
|
|
const normalized = candidate.toLowerCase()
|
|
if (seen.has(normalized)) continue
|
|
seen.add(normalized)
|
|
addresses.push(candidate)
|
|
if (addresses.length >= 12) return addresses
|
|
}
|
|
}
|
|
|
|
return addresses
|
|
}, [recentTransactions])
|
|
|
|
const handleOpenAddress = (event: React.FormEvent) => {
|
|
event.preventDefault()
|
|
const normalized = normalizeAddress(query)
|
|
if (!normalized) return
|
|
router.push(`/addresses/${normalized}`)
|
|
}
|
|
|
|
return (
|
|
<div className="container mx-auto px-4 py-6 sm:py-8">
|
|
<PageIntro
|
|
eyebrow="Address Discovery"
|
|
title="Addresses"
|
|
description="Open any Chain 138 address directly, revisit saved watchlist entries, or branch into recent activity discovered from indexed transactions."
|
|
actions={[
|
|
{ href: '/watchlist', label: 'Open watchlist' },
|
|
{ href: '/transactions', label: 'Recent transactions' },
|
|
{ href: '/search', label: 'Search explorer' },
|
|
]}
|
|
/>
|
|
|
|
<Card className="mb-6" title="Open An Address">
|
|
<form onSubmit={handleOpenAddress} className="flex flex-col gap-3 md:flex-row">
|
|
<input
|
|
type="text"
|
|
value={query}
|
|
onChange={(event) => setQuery(event.target.value)}
|
|
placeholder="0x..."
|
|
className="flex-1 rounded-lg border border-gray-300 px-4 py-2 focus:outline-none focus:ring-2 focus:ring-primary-500"
|
|
/>
|
|
<button
|
|
type="submit"
|
|
disabled={!normalizeAddress(query)}
|
|
className="rounded-lg bg-primary-600 px-6 py-2 text-white hover:bg-primary-700 disabled:cursor-not-allowed disabled:opacity-50"
|
|
>
|
|
Open address
|
|
</button>
|
|
</form>
|
|
<p className="mt-3 text-sm text-gray-600 dark:text-gray-400">
|
|
Open any Chain 138 address directly, or jump into your saved watchlist below.
|
|
</p>
|
|
</Card>
|
|
|
|
<div className="grid gap-6 lg:grid-cols-2">
|
|
<Card title="Saved Watchlist">
|
|
{watchlist.length === 0 ? (
|
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
|
No saved addresses yet. Address detail pages let you add entries to the shared explorer watchlist.
|
|
</p>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{watchlist.map((entry) => (
|
|
<Link key={entry} href={`/addresses/${entry}`} className="block text-primary-600 hover:underline">
|
|
<Address address={entry} showCopy={false} />
|
|
</Link>
|
|
))}
|
|
<div className="pt-2">
|
|
<Link href="/watchlist" className="text-sm text-primary-600 hover:underline">
|
|
Open the full watchlist →
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</Card>
|
|
|
|
<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.
|
|
</p>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{activeAddresses.map((entry) => (
|
|
<Link key={entry} href={`/addresses/${entry}`} className="block text-primary-600 hover:underline">
|
|
<Address address={entry} showCopy={false} />
|
|
</Link>
|
|
))}
|
|
</div>
|
|
)}
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
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 initialRecentTransactions = Array.isArray(transactionsResult?.items)
|
|
? transactionsResult.items.map((item) => normalizeTransaction(item as never, chainId))
|
|
: []
|
|
|
|
return {
|
|
props: {
|
|
initialRecentTransactions: serializeRecentTransactions(initialRecentTransactions),
|
|
},
|
|
}
|
|
}
|