diff --git a/docs/EXPLORER_API_ACCESS.md b/docs/EXPLORER_API_ACCESS.md index 1d2cc9e..5346619 100644 --- a/docs/EXPLORER_API_ACCESS.md +++ b/docs/EXPLORER_API_ACCESS.md @@ -28,6 +28,19 @@ Why: - Setting `NEXT_PUBLIC_API_URL=https://explorer.d-bis.org/api` will incorrectly produce requests like `/api/api/v2/*`. - Token aggregation remains under `/token-aggregation/api/v1/*` and is linked separately by the frontend. +### Local frontend validation + +From `frontend/`, the current local validation flow is: + +```bash +npm run build:check +BASE_URL=http://127.0.0.1:3000 npm run smoke:routes +``` + +- `build:check` runs lint, type-check, and a production build. +- `smoke:routes` performs a lightweight route and content sweep against a running frontend instance. +- `npm run dev` now binds to `127.0.0.1` by default, but still honors `HOST` and `PORT` overrides. + --- ## CSP blocks eval / “script-src blocked” diff --git a/frontend/package.json b/frontend/package.json index 70dfe56..8965767 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -4,9 +4,10 @@ "private": true, "packageManager": "pnpm@10.0.0", "scripts": { - "dev": "next dev", + "dev": "sh -c 'HOST=${HOST:-127.0.0.1}; PORT=${PORT:-3000}; next dev -H \"$HOST\" -p \"$PORT\"'", "build": "next build", "build:check": "npm run lint && npm run type-check && npm run build", + "smoke:routes": "node ./scripts/smoke-routes.mjs", "start": "PORT=${PORT:-3000} node .next/standalone/server.js", "start:next": "next start", "lint": "next lint", diff --git a/frontend/scripts/smoke-routes.mjs b/frontend/scripts/smoke-routes.mjs new file mode 100644 index 0000000..12d84d9 --- /dev/null +++ b/frontend/scripts/smoke-routes.mjs @@ -0,0 +1,64 @@ +const baseUrl = (process.env.BASE_URL || 'http://127.0.0.1:3000').replace(/\/$/, '') + +const checks = [ + { path: '/', expect: ['/addresses', '/transactions', '/watchlist', '/pools'] }, + { path: '/blocks', expect: ['Latest Blocks', 'View blockchain blocks', 'Blocks'] }, + { path: '/transactions', expect: ['Latest Transactions', 'Recent Transactions', 'Transactions'] }, + { path: '/addresses', expect: ['Open An Address', 'Recently Active Addresses', 'Saved Watchlist'] }, + { path: '/tokens', expect: ['Find A Token', 'Common token searches', 'Tokens'] }, + { path: '/pools', expect: ['Canonical PMM routes', 'Pool operation shortcuts', 'Pools'] }, + { path: '/watchlist', expect: ['Saved Addresses', 'Watchlist', 'Export JSON'] }, + { path: '/search?q=cUSDT', expect: ['Search Results', 'Results for', 'Search'] }, + { path: '/blocks/1', expect: ['Loading block details', 'Block Details'] }, + { + path: '/transactions/0x0000000000000000000000000000000000000000000000000000000000000000', + expect: ['Loading transaction details', 'Transaction Details', 'Transaction not found'], + }, + { + path: '/addresses/0x0000000000000000000000000000000000000000', + expect: ['Loading address details', 'Address Details', 'Open An Address'], + }, +] + +function hasExpectedBody(text, expectedSnippets) { + return expectedSnippets.some((snippet) => text.includes(snippet)) +} + +async function run() { + let failures = 0 + + for (const check of checks) { + const url = `${baseUrl}${check.path}` + + try { + const response = await fetch(url, { redirect: 'follow' }) + const body = await response.text() + + if (!response.ok) { + console.error(`FAIL ${check.path}: HTTP ${response.status}`) + failures += 1 + continue + } + + if (!hasExpectedBody(body, check.expect)) { + console.error(`FAIL ${check.path}: expected one of ${check.expect.join(' | ')}`) + failures += 1 + continue + } + + console.log(`OK ${check.path}`) + } catch (error) { + console.error(`FAIL ${check.path}: ${error instanceof Error ? error.message : String(error)}`) + failures += 1 + } + } + + if (failures > 0) { + process.exitCode = 1 + return + } + + console.log(`All ${checks.length} route checks passed for ${baseUrl}`) +} + +run() diff --git a/frontend/src/components/common/Navbar.tsx b/frontend/src/components/common/Navbar.tsx index ed0fa0d..d564895 100644 --- a/frontend/src/components/common/Navbar.tsx +++ b/frontend/src/components/common/Navbar.tsx @@ -63,7 +63,7 @@ function DropdownItem({ if (external) { return (
  • - + {icon} {children} diff --git a/frontend/src/pages/pools/index.tsx b/frontend/src/pages/pools/index.tsx index c05cc29..8889dd9 100644 --- a/frontend/src/pages/pools/index.tsx +++ b/frontend/src/pages/pools/index.tsx @@ -5,7 +5,7 @@ import { Card } from '@/libs/frontend-ui-primitives' const poolCards = [ { - title: 'Canonical PMM Matrix', + title: 'Canonical PMM routes', description: 'Review the public Chain 138 DODO PMM route matrix, live pool freshness, and payload examples.', href: '/liquidity', label: 'Open liquidity access', @@ -25,6 +25,24 @@ const poolCards = [ }, ] +const shortcutCards = [ + { + title: 'cUSDT / USDT', + description: 'Open the canonical direct stable route coverage and compare the live pool snapshot.', + href: '/liquidity', + }, + { + title: 'cUSDC / USDC', + description: 'Check the public stable bridge route and inspect the live reserves block.', + href: '/liquidity', + }, + { + title: 'cUSDT / cXAUC', + description: 'Review one of the live gold-backed route families from the liquidity access page.', + href: '/liquidity', + }, +] + export default function PoolsPage() { return (
    @@ -48,6 +66,23 @@ export default function PoolsPage() { ))}
    + +
    + +
    + {shortcutCards.map((card) => ( + +
    {card.title}
    +

    {card.description}

    + + ))} +
    +
    +
    ) } diff --git a/frontend/src/pages/tokens/index.tsx b/frontend/src/pages/tokens/index.tsx index 39cd1d6..c551c55 100644 --- a/frontend/src/pages/tokens/index.tsx +++ b/frontend/src/pages/tokens/index.tsx @@ -5,6 +5,15 @@ import { useRouter } from 'next/router' import { useState } from 'react' import { Card } from '@/libs/frontend-ui-primitives' +const quickSearches = [ + { label: 'cUSDT', description: 'Canonical bridged USDT liquidity and address results.' }, + { label: 'cUSDC', description: 'Canonical bridged USDC routes and address coverage.' }, + { label: 'cXAUC', description: 'Gold-backed cXAUC pools and token references.' }, + { label: 'cXAUT', description: 'Gold-backed cXAUT references and search coverage.' }, + { label: 'cEURT', description: 'EUR liquidity and cXAUC-connected route coverage.' }, + { label: 'USDT', description: 'Native-side USDT routes and address discovery.' }, +] + function normalizeAddress(value: string) { const trimmed = value.trim() return /^0x[a-fA-F0-9]{40}$/.test(trimmed) ? trimmed : '' @@ -75,6 +84,23 @@ export default function TokensPage() { + +
    + +
    + {quickSearches.map((token) => ( + +
    {token.label}
    +

    {token.description}

    + + ))} +
    +
    +
    ) } diff --git a/frontend/src/pages/watchlist/index.tsx b/frontend/src/pages/watchlist/index.tsx index 9c1fdbd..375f7f2 100644 --- a/frontend/src/pages/watchlist/index.tsx +++ b/frontend/src/pages/watchlist/index.tsx @@ -27,15 +27,72 @@ export default function WatchlistPage() { }) } + const exportWatchlist = () => { + try { + const blob = new Blob([JSON.stringify(entries, null, 2)], { type: 'application/json' }) + const url = URL.createObjectURL(blob) + const anchor = document.createElement('a') + anchor.href = url + anchor.download = 'explorer-watchlist.json' + anchor.click() + URL.revokeObjectURL(url) + } catch {} + } + + const importWatchlist = (event: React.ChangeEvent) => { + const file = event.target.files?.[0] + if (!file) return + + file.text().then((text) => { + try { + const parsed = JSON.parse(text) + const next = Array.isArray(parsed) + ? parsed.filter((entry): entry is string => typeof entry === 'string') + : [] + setEntries(next) + window.localStorage.setItem('explorerWatchlist', JSON.stringify(next)) + } catch {} + }).catch(() => {}) + + event.target.value = '' + } + return (

    Watchlist

    - {entries.length === 0 ? ( +

    - Your watchlist is empty. Add an address from its detail page to keep it here. + {entries.length === 0 ? 'No saved entries yet.' : `${entries.length} saved ${entries.length === 1 ? 'address' : 'addresses'}.`}

    +
    + + +
    +
    + {entries.length === 0 ? ( +
    +

    Your watchlist is empty. Add an address from its detail page to keep it here.

    +
    + + Browse addresses → + + + Search the explorer → + +
    +
    ) : (
    {entries.map((entry) => (