diff --git a/src/app/lookup/page.tsx b/src/app/lookup/page.tsx index 7f2b2d6..57e8767 100644 --- a/src/app/lookup/page.tsx +++ b/src/app/lookup/page.tsx @@ -1,44 +1,147 @@ -// /lookup?addr=X — server component that disambiguates a base58 address -// (wallet vs token mint) and redirects to the right detail page. +// /lookup?addr=X — closed-beta entry for the token card. // -// Mirrors the backend GET /lookup handler in -// integrations/mcp/http_api.py — duplicated client-side as a Next server -// component so the user lands on /operator/X or /token/X without an extra -// hop through the API. Unknown addresses default to the operator page. +// The home search sends every base58 address here. Only TOKEN MINTS are open: +// mint → 307 /token/X +// wallet/unknown → NO redirect to /operator (wallet surfaces are still closed); +// render a plain "wallet lookups are not open yet" message +// invalid input → plain message (never 500) // -// Spec: internal/marketing/strategy/WIREFRAME_v5.md §3.1 +// Mint detection (a wallet is NOT told apart by /v1/token: the API live-scans any +// address and answers UNKNOWN): +// 1. /v1/token/X already knows it (tier != UNKNOWN, or has a symbol) +// 2. otherwise Solana RPC getAccountInfo: parsed account type "mint" (SPL / Token-2022) +// Operator lookups (/v1/operator) are deliberately NOT called here. +import Link from "next/link"; import { redirect } from "next/navigation"; -import { quickEasyLookup } from "@/lib/scan-resolver"; - -const BASE58_RE = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/; +import { SiteTopbar } from "@/components/SiteTopbar"; +import { Footer } from "@/components/Footer"; +import { BASE58_RE } from "@/lib/token-card"; export const dynamic = "force-dynamic"; +export const metadata = { title: "Lookup", robots: { index: false, follow: false } }; + +const API = process.env.NEXT_PUBLIC_API_URL || "https://api.solsentry.app"; +const SOLANA_RPC = process.env.SOLANA_RPC_URL || "https://api.mainnet-beta.solana.com"; interface PageProps { - searchParams: Promise<{ addr?: string }>; + searchParams: Promise<{ addr?: string | string[] }>; +} + +type Verdict = "token" | "not_token" | "unverifiable"; + +async function tokenKnownToApi(addr: string): Promise { + try { + const r = await fetch(`${API}/v1/token/${encodeURIComponent(addr)}`, { + cache: "no-store", + signal: AbortSignal.timeout(8_000), + }); + if (!r.ok) return false; + const t = await r.json(); + // NB: launch_platform is "unknown" even for plain wallets → not a usable signal. + return !!t && t.known !== false && (t.risk_level !== "UNKNOWN" || !!t.symbol); + } catch { + return false; + } +} + +async function isMintOnChain(addr: string): Promise<"yes" | "no" | "error"> { + try { + const r = await fetch(SOLANA_RPC, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + jsonrpc: "2.0", + id: 1, + method: "getAccountInfo", + params: [addr, { encoding: "jsonParsed" }], + }), + cache: "no-store", + signal: AbortSignal.timeout(8_000), + }); + if (!r.ok) return "error"; + const j = await r.json(); + if (j?.error || !j?.result) return "error"; + return j.result.value?.data?.parsed?.type === "mint" ? "yes" : "no"; + } catch { + return "error"; + } +} + +async function classify(addr: string): Promise { + if (await tokenKnownToApi(addr)) return "token"; + const onchain = await isMintOnChain(addr); + if (onchain === "yes") return "token"; + if (onchain === "no") return "not_token"; + return "unverifiable"; +} + +function Message({ title, body }: { title: string; body: string }) { + return ( + <> + +
+
+
+
+ Beta — token lookups only +
+

{title}

+

+ {body} +

+ + ← Back to search + +
+
+
+