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="Liquidity 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>
|
||
|
|
)
|
||
|
|
}
|