- 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.
238 lines
9.8 KiB
TypeScript
238 lines
9.8 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useMemo, useState } from 'react'
|
|
import Link from 'next/link'
|
|
import { Card } from '@/libs/frontend-ui-primitives'
|
|
import { configApi, type TokenListResponse } from '@/services/api/config'
|
|
import {
|
|
aggregateLiquidityPools,
|
|
getRouteBackedPoolAddresses,
|
|
selectFeaturedLiquidityTokens,
|
|
} from '@/services/api/liquidity'
|
|
import { routesApi, type MissionControlLiquidityPool, type RouteMatrixResponse } from '@/services/api/routes'
|
|
import { formatCurrency, formatNumber, truncateMiddle } from './OperationsPageShell'
|
|
|
|
interface TokenPoolRecord {
|
|
symbol: string
|
|
pools: MissionControlLiquidityPool[]
|
|
}
|
|
|
|
export default function PoolsOperationsPage() {
|
|
const [tokenList, setTokenList] = useState<TokenListResponse | null>(null)
|
|
const [routeMatrix, setRouteMatrix] = useState<RouteMatrixResponse | null>(null)
|
|
const [tokenPoolRecords, setTokenPoolRecords] = useState<TokenPoolRecord[]>([])
|
|
const [loadingError, setLoadingError] = useState<string | null>(null)
|
|
|
|
useEffect(() => {
|
|
let cancelled = false
|
|
|
|
const load = async () => {
|
|
const [tokenListResult, routeMatrixResult] = await Promise.allSettled([
|
|
configApi.getTokenList(),
|
|
routesApi.getRouteMatrix(),
|
|
])
|
|
|
|
if (cancelled) return
|
|
|
|
if (tokenListResult.status === 'fulfilled') setTokenList(tokenListResult.value)
|
|
if (routeMatrixResult.status === 'fulfilled') setRouteMatrix(routeMatrixResult.value)
|
|
|
|
if (tokenListResult.status === 'fulfilled') {
|
|
const featuredTokens = selectFeaturedLiquidityTokens(tokenListResult.value.tokens || [])
|
|
const poolResults = await Promise.allSettled(
|
|
featuredTokens.map(async (token) => ({
|
|
symbol: token.symbol,
|
|
pools: (await routesApi.getTokenPools(token.address)).pools || [],
|
|
}))
|
|
)
|
|
|
|
if (!cancelled) {
|
|
setTokenPoolRecords(
|
|
poolResults
|
|
.filter((result): result is PromiseFulfilledResult<TokenPoolRecord> => result.status === 'fulfilled')
|
|
.map((result) => result.value)
|
|
)
|
|
}
|
|
}
|
|
|
|
if (tokenListResult.status === 'rejected' && routeMatrixResult.status === 'rejected') {
|
|
setLoadingError('Live pool inventory is temporarily unavailable from the public explorer APIs.')
|
|
}
|
|
}
|
|
|
|
load().catch((error) => {
|
|
if (!cancelled) {
|
|
setLoadingError(
|
|
error instanceof Error ? error.message : 'Live pool inventory is temporarily unavailable from the public explorer APIs.'
|
|
)
|
|
}
|
|
})
|
|
|
|
return () => {
|
|
cancelled = true
|
|
}
|
|
}, [])
|
|
|
|
const featuredTokens = useMemo(
|
|
() => selectFeaturedLiquidityTokens(tokenList?.tokens || []),
|
|
[tokenList?.tokens]
|
|
)
|
|
const aggregatedPools = useMemo(
|
|
() => aggregateLiquidityPools(tokenPoolRecords),
|
|
[tokenPoolRecords]
|
|
)
|
|
const routeBackedPoolAddresses = useMemo(
|
|
() => getRouteBackedPoolAddresses(routeMatrix),
|
|
[routeMatrix]
|
|
)
|
|
const topPools = aggregatedPools.slice(0, 9)
|
|
|
|
return (
|
|
<div className="container mx-auto px-4 py-6 sm:py-8">
|
|
<div className="mb-6 max-w-4xl sm:mb-8">
|
|
<div className="mb-3 inline-flex rounded-full border border-primary-200 bg-primary-50 px-3 py-1 text-xs font-semibold uppercase tracking-[0.2em] text-primary-700">
|
|
Live Pool Inventory
|
|
</div>
|
|
<h1 className="mb-3 text-3xl font-bold text-gray-900 dark:text-white sm:text-4xl">
|
|
Pools
|
|
</h1>
|
|
<p className="text-base leading-7 text-gray-600 dark:text-gray-400 sm:text-lg sm:leading-8">
|
|
This page now summarizes the live pool inventory discovered through mission-control token
|
|
pool endpoints and cross-checks it against the current route matrix.
|
|
</p>
|
|
</div>
|
|
|
|
{loadingError ? (
|
|
<Card className="mb-6 border border-red-200 bg-red-50/70 dark:border-red-900/50 dark:bg-red-950/20">
|
|
<p className="text-sm leading-6 text-red-900 dark:text-red-100">{loadingError}</p>
|
|
</Card>
|
|
) : null}
|
|
|
|
<div className="mb-8 grid gap-4 md:grid-cols-3">
|
|
<Card className="border border-emerald-200 bg-emerald-50/70 dark:border-emerald-900/50 dark:bg-emerald-950/20">
|
|
<div className="text-sm font-semibold uppercase tracking-wide text-emerald-800 dark:text-emerald-100">
|
|
Unique pools
|
|
</div>
|
|
<div className="mt-2 text-3xl font-bold text-gray-900 dark:text-white">
|
|
{formatNumber(aggregatedPools.length)}
|
|
</div>
|
|
<div className="mt-2 text-sm text-gray-700 dark:text-gray-300">
|
|
Discovered across {formatNumber(featuredTokens.length)} featured Chain 138 tokens.
|
|
</div>
|
|
</Card>
|
|
<Card className="border border-sky-200 bg-sky-50/70 dark:border-sky-900/50 dark:bg-sky-950/20">
|
|
<div className="text-sm font-semibold uppercase tracking-wide text-sky-800 dark:text-sky-100">
|
|
Route-backed pools
|
|
</div>
|
|
<div className="mt-2 text-3xl font-bold text-gray-900 dark:text-white">
|
|
{formatNumber(routeBackedPoolAddresses.length)}
|
|
</div>
|
|
<div className="mt-2 text-sm text-gray-700 dark:text-gray-300">
|
|
Unique pool addresses referenced by the live route matrix.
|
|
</div>
|
|
</Card>
|
|
<Card>
|
|
<div className="text-sm text-gray-500 dark:text-gray-400">Featured coverage</div>
|
|
<div className="mt-2 text-3xl font-bold text-gray-900 dark:text-white">
|
|
{formatNumber(tokenPoolRecords.filter((record) => record.pools.length > 0).length)}
|
|
</div>
|
|
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
|
Featured tokens currently returning at least one live pool.
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="mb-8">
|
|
<Card title="Live Pool Cards">
|
|
<div className="grid gap-4 lg:grid-cols-3">
|
|
{topPools.map((pool) => (
|
|
<div
|
|
key={pool.address}
|
|
className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40"
|
|
>
|
|
<div className="text-base font-semibold text-gray-900 dark:text-white">
|
|
{(pool.token0?.symbol || '?') + ' / ' + (pool.token1?.symbol || '?')}
|
|
</div>
|
|
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
|
{pool.dex || 'Unknown DEX'} · TVL {formatCurrency(pool.tvl)}
|
|
</div>
|
|
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
|
{truncateMiddle(pool.address, 10, 8)}
|
|
</div>
|
|
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
|
Seen from {pool.sourceSymbols.join(', ')}
|
|
</div>
|
|
</div>
|
|
))}
|
|
{topPools.length === 0 ? (
|
|
<p className="text-sm text-gray-600 dark:text-gray-400">No live pools available right now.</p>
|
|
) : null}
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="mb-8 grid gap-6 lg:grid-cols-[1fr_1fr]">
|
|
<Card title="Featured Token Pool Counts">
|
|
<div className="space-y-4">
|
|
{featuredTokens.map((token) => {
|
|
const record = tokenPoolRecords.find((entry) => entry.symbol === token.symbol)
|
|
return (
|
|
<div
|
|
key={token.address}
|
|
className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40"
|
|
>
|
|
<div className="flex items-center justify-between gap-3">
|
|
<div>
|
|
<div className="text-base font-semibold text-gray-900 dark:text-white">
|
|
{token.symbol}
|
|
</div>
|
|
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
|
{token.name || 'Unnamed token'}
|
|
</div>
|
|
</div>
|
|
<div className="text-sm font-semibold text-gray-900 dark:text-white">
|
|
{formatNumber(record?.pools.length || 0)} pools
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</Card>
|
|
|
|
<Card title="Pool operation shortcuts">
|
|
<div className="space-y-4 text-sm leading-6 text-gray-600 dark:text-gray-400">
|
|
<p>
|
|
The broader liquidity page now shows live route, planner, and pool access together.
|
|
</p>
|
|
<p>
|
|
The current route matrix publishes {formatNumber(routeMatrix?.counts?.liveSwapRoutes)} live
|
|
swap routes and {formatNumber(routeMatrix?.counts?.liveBridgeRoutes)} bridge routes.
|
|
</p>
|
|
<div className="flex flex-wrap gap-3">
|
|
<Link
|
|
href="/liquidity"
|
|
className="rounded-full bg-primary-600 px-4 py-2 text-sm font-medium text-white transition hover:bg-primary-700"
|
|
>
|
|
Open liquidity access
|
|
</Link>
|
|
<Link
|
|
href="/routes"
|
|
className="rounded-full border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 transition hover:border-primary-400 hover:text-primary-700 dark:border-gray-600 dark:text-gray-300 dark:hover:text-primary-300"
|
|
>
|
|
Open routes page
|
|
</Link>
|
|
<Link
|
|
href="/wallet"
|
|
className="rounded-full border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 transition hover:border-primary-400 hover:text-primary-700 dark:border-gray-600 dark:text-gray-300 dark:hover:text-primary-300"
|
|
>
|
|
Open wallet tools
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|