diff --git a/CLAUDE.md b/CLAUDE.md
index 580fec7..76696d8 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -65,13 +65,18 @@ Required keys: MAPBOX_TOKEN, PINATA_API_KEY, PINATA_API_SECRET, KAFKA_BOOTSTRAP,
## Branch Strategy
- `main` — production-ready code
-- `develop` — integration branch
-- Feature branches off `develop`, PRs back to `develop`
+- `ready-to-go` — current integration branch where active work lands (ahead of `main`)
+- Feature branches off `ready-to-go`, PRs back to `ready-to-go`
+
+> ⚠️ Divergence to reconcile: CI (`ci.yml`) and testnet deploy (`deploy-testnet.yml`)
+> still trigger on a `develop` branch that does not currently exist on the remote.
+> Either create `develop` as the integration branch or repoint those workflows at
+> `ready-to-go`.
## CI/CD
- GitHub Actions: Aiken build/test, Hardhat compile/test/coverage, Python pytest, Node typecheck/lint/build
-- Testnet deploy: push to `develop` triggers Cardano Pre-Prod + Base Sepolia deployment
+- Testnet deploy: push to `develop` triggers Cardano Pre-Prod + Base Sepolia deployment (see divergence note above — `develop` is currently absent)
- Vercel: `apps/web` auto-deploys from `main`
## Conventions
diff --git a/apps/web/package.json b/apps/web/package.json
index c1566a4..909c597 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -6,7 +6,9 @@
"dev": "next dev --webpack",
"build": "next build --webpack",
"start": "next start",
- "lint": "next lint"
+ "lint": "next lint",
+ "test": "vitest run",
+ "test:watch": "vitest"
},
"dependencies": {
"@magic-sdk/admin": "^2.8.2",
@@ -38,6 +40,7 @@
"autoprefixer": "^10.4.19",
"postcss": "^8.4.38",
"tailwindcss": "^3.4.3",
- "typescript": "^5"
+ "typescript": "^5",
+ "vitest": "^4.1.9"
}
}
diff --git a/apps/web/src/app/api/hexes/holds/route.test.ts b/apps/web/src/app/api/hexes/holds/route.test.ts
new file mode 100644
index 0000000..69a6f66
--- /dev/null
+++ b/apps/web/src/app/api/hexes/holds/route.test.ts
@@ -0,0 +1,25 @@
+import { describe, it, expect } from 'vitest'
+import { GET } from './route'
+import { issueGlobalHold } from '@/lib/global-hold-store'
+
+// In-process test — a single Vitest process shares one memKv, so this exercises the
+// real route handler end-to-end (live dev can't, because memKv doesn't persist across
+// Next's per-request module boundaries without Redis configured).
+describe('GET /api/hexes/holds', () => {
+ it('serves active holds as a GeoJSON FeatureCollection', async () => {
+ const SYD = '84be0e3ffffffff'
+ await issueGlobalHold({ hexId: SYD, email: 'overlay@example.com', lat: -33.87, lng: 151.21 })
+
+ const res = await GET()
+ const body = (await res.json()) as {
+ type: string
+ features: Array<{ geometry: { type: string }; properties: { id: string; status: string } }>
+ }
+
+ expect(body.type).toBe('FeatureCollection')
+ const feature = body.features.find((f) => f.properties.id === SYD)
+ expect(feature).toBeDefined()
+ expect(feature?.geometry.type).toBe('Polygon')
+ expect(feature?.properties.status).toBe('held')
+ })
+})
diff --git a/apps/web/src/app/api/hexes/holds/route.ts b/apps/web/src/app/api/hexes/holds/route.ts
new file mode 100644
index 0000000..5d66807
--- /dev/null
+++ b/apps/web/src/app/api/hexes/holds/route.ts
@@ -0,0 +1,18 @@
+import { NextResponse } from 'next/server'
+import { listActiveHolds } from '@/lib/global-hold-store'
+import { hexToGeoJSON } from '@/lib/h3'
+
+export const dynamic = 'force-dynamic'
+
+/**
+ * Active global holds as a GeoJSON FeatureCollection, for the map overlay.
+ * These are off-chain reservations of Res-4 cells outside the curated 200.
+ */
+export async function GET() {
+ const holds = await listActiveHolds()
+ const features = holds.map((h) => ({
+ ...hexToGeoJSON(h.hexId),
+ properties: { id: h.hexId, status: 'held', heldAt: h.heldAt, expiresAt: h.expiresAt },
+ }))
+ return NextResponse.json({ type: 'FeatureCollection', features })
+}
diff --git a/apps/web/src/app/api/hexes/resolve/route.ts b/apps/web/src/app/api/hexes/resolve/route.ts
new file mode 100644
index 0000000..623300f
--- /dev/null
+++ b/apps/web/src/app/api/hexes/resolve/route.ts
@@ -0,0 +1,33 @@
+import { NextResponse } from 'next/server'
+import { resolveCellStatus } from '@/lib/cell-status'
+import { resolveContainingCell } from '@/lib/hex-lookup'
+
+export const dynamic = 'force-dynamic'
+
+/**
+ * Resolve a cell's reservation status for the address-lookup flow.
+ * GET ?hex=
→ status of that cell
+ * GET ?lat=&lng= → status of the Res-4 cell containing the point
+ * Returns { hexId, resolution, status, lat, lng, priceUsd }.
+ */
+export async function GET(req: Request) {
+ const { searchParams } = new URL(req.url)
+ const hexParam = searchParams.get('hex')
+ const lat = Number(searchParams.get('lat'))
+ const lng = Number(searchParams.get('lng'))
+
+ let hexId: string
+ if (hexParam) {
+ hexId = hexParam
+ } else if (Number.isFinite(lat) && Number.isFinite(lng)) {
+ hexId = resolveContainingCell(lat, lng)
+ } else {
+ return NextResponse.json({ error: 'Provide ?hex= or ?lat=&lng=' }, { status: 400 })
+ }
+
+ try {
+ return NextResponse.json(await resolveCellStatus(hexId))
+ } catch {
+ return NextResponse.json({ error: 'Invalid H3 cell' }, { status: 400 })
+ }
+}
diff --git a/apps/web/src/app/api/holds/route.ts b/apps/web/src/app/api/holds/route.ts
new file mode 100644
index 0000000..bd3f19b
--- /dev/null
+++ b/apps/web/src/app/api/holds/route.ts
@@ -0,0 +1,87 @@
+import { NextResponse } from 'next/server'
+import {
+ issueGlobalHold,
+ getGlobalHold,
+ releaseGlobalHold,
+ listHoldsByEmail,
+} from '@/lib/global-hold-store'
+import { resolveContainingCell } from '@/lib/hex-lookup'
+import nativeHexesData from '@/data/genesis-native-hexes.json'
+
+const NATIVE_HEX_SET = new Set(Object.keys(nativeHexesData as Record))
+
+export const dynamic = 'force-dynamic'
+
+/**
+ * Global hex holds — off-chain exclusive reservations of Res-4 cells outside the
+ * curated Genesis 200. Never mints, never consumes a Genesis edition.
+ *
+ * POST { email, lat, lng, referrerId? } → place a hold (server derives the cell)
+ * GET ?email=… → a user's holds | ?hex=… → a single cell's hold
+ * DELETE { hexId, email } → release your own hold
+ */
+export async function POST(req: Request) {
+ try {
+ const { email, lat, lng, referrerId } = await req.json()
+
+ if (!email || typeof email !== 'string') {
+ return NextResponse.json({ error: 'Missing required field: email' }, { status: 400 })
+ }
+ if (!Number.isFinite(lat) || !Number.isFinite(lng)) {
+ return NextResponse.json({ error: 'lat and lng must be numbers' }, { status: 400 })
+ }
+
+ // Derive the cell server-side so a hold always matches its coordinates.
+ const hexId = resolveContainingCell(lat, lng)
+
+ // Native-reserved cells are held for Native Tribes first — not publicly holdable.
+ if (NATIVE_HEX_SET.has(hexId)) {
+ return NextResponse.json(
+ { error: 'This cell is on tribal land and reserved for Native Tribes', nativeReserved: true },
+ { status: 403 },
+ )
+ }
+
+ const result = await issueGlobalHold({ hexId, email, lat, lng, referrerId })
+ if (!result.ok) {
+ return NextResponse.json(
+ { error: result.error, hexId, alreadyHeld: true },
+ { status: 409 },
+ )
+ }
+
+ return NextResponse.json({ ok: true, hold: result.hold }, { status: 201 })
+ } catch {
+ return NextResponse.json({ error: 'Invalid request' }, { status: 400 })
+ }
+}
+
+export async function GET(req: Request) {
+ const { searchParams } = new URL(req.url)
+ const email = searchParams.get('email')
+ const hex = searchParams.get('hex')
+
+ if (hex) {
+ return NextResponse.json({ hold: await getGlobalHold(hex) })
+ }
+ if (email) {
+ return NextResponse.json({ holds: await listHoldsByEmail(email) })
+ }
+ return NextResponse.json({ error: 'Provide ?email= or ?hex=' }, { status: 400 })
+}
+
+export async function DELETE(req: Request) {
+ try {
+ const { hexId, email } = await req.json()
+ if (!hexId || !email) {
+ return NextResponse.json({ error: 'Missing required fields: hexId, email' }, { status: 400 })
+ }
+ const result = await releaseGlobalHold(hexId, email)
+ if (!result.ok) {
+ return NextResponse.json({ error: result.reason }, { status: 403 })
+ }
+ return NextResponse.json({ ok: true })
+ } catch {
+ return NextResponse.json({ error: 'Invalid request' }, { status: 400 })
+ }
+}
diff --git a/apps/web/src/app/lookup/page.tsx b/apps/web/src/app/lookup/page.tsx
new file mode 100644
index 0000000..674700d
--- /dev/null
+++ b/apps/web/src/app/lookup/page.tsx
@@ -0,0 +1,20 @@
+import AddressHexLookup from '@/components/AddressHexLookup'
+
+export const metadata = {
+ title: 'Address → Hex Lookup',
+ description: 'Find the Res-4 hex cell that contains your address.',
+}
+
+export default function LookupPage() {
+ return (
+
+ Find your hex
+
+ Enter your address to see the Res-4 H3 cell (~1,770 km²) that contains it.
+
+
+
+ )
+}
diff --git a/apps/web/src/components/AddressHexLookup.tsx b/apps/web/src/components/AddressHexLookup.tsx
new file mode 100644
index 0000000..5dff0a0
--- /dev/null
+++ b/apps/web/src/components/AddressHexLookup.tsx
@@ -0,0 +1,259 @@
+'use client'
+
+import { useEffect, useRef, useState } from 'react'
+import { Search, Loader2, MapPin, Hexagon, Check } from 'lucide-react'
+import HexBoundaryPreview from './HexBoundaryPreview'
+import { resolveContainingCell, neighborCells } from '@/lib/hex-lookup'
+import { cellStatusCta, type CellStatusResult } from '@/lib/cell-status'
+import { parseForwardSuggestions, type AddressSuggestion } from '@/lib/mapbox-geocode'
+import { formatGenesisListingUsd } from '@/lib/h3'
+
+type Selected = { hexId: string; label: string; sublabel: string; lat: number; lng: number }
+
+/**
+ * Address → Hex lookup & reserve (Slices 1–4).
+ * Type an address, see the Res-4 cell that contains it, and act on its status:
+ * mint a curated cell, place a global hold, or pick an available neighbour.
+ */
+export default function AddressHexLookup({ className = '' }: { className?: string }) {
+ const token = process.env.NEXT_PUBLIC_MAPBOX_TOKEN
+
+ const [query, setQuery] = useState('')
+ const [suggestions, setSuggestions] = useState([])
+ const [loading, setLoading] = useState(false)
+ const [open, setOpen] = useState(false)
+ const justSelected = useRef(false)
+
+ const [selected, setSelected] = useState(null)
+ const [status, setStatus] = useState(null)
+ const [statusLoading, setStatusLoading] = useState(false)
+ const [nearby, setNearby] = useState([])
+
+ const [holdEmail, setHoldEmail] = useState('')
+ const [holdState, setHoldState] = useState<'idle' | 'submitting' | 'error'>('idle')
+
+ // Debounced forward geocode (Mapbox Search API v6 forward, autocomplete).
+ useEffect(() => {
+ if (!token) return
+ if (justSelected.current) {
+ justSelected.current = false
+ return
+ }
+ const q = query.trim()
+ if (q.length < 3) {
+ setSuggestions([])
+ return
+ }
+ let cancelled = false
+ setLoading(true)
+ const handle = setTimeout(async () => {
+ try {
+ const params = new URLSearchParams({ q, autocomplete: 'true', limit: '5', access_token: token })
+ const res = await fetch(`https://api.mapbox.com/search/geocode/v6/forward?${params}`)
+ const data = await res.json()
+ if (cancelled) return
+ setSuggestions(parseForwardSuggestions(data))
+ setOpen(true)
+ } catch {
+ if (!cancelled) setSuggestions([])
+ } finally {
+ if (!cancelled) setLoading(false)
+ }
+ }, 300)
+ return () => {
+ cancelled = true
+ clearTimeout(handle)
+ }
+ }, [query, token])
+
+ // Resolve the selected cell's status (and refetch after a hold).
+ async function loadStatus(sel: Selected) {
+ setStatusLoading(true)
+ setNearby([])
+ try {
+ const res = await fetch(`/api/hexes/resolve?hex=${sel.hexId}`)
+ const data: CellStatusResult = await res.json()
+ setStatus(data)
+ // For taken / native cells, surface available neighbours.
+ if (data.status === 'curated-taken' || data.status === 'global-held' || data.status === 'native') {
+ const ring = neighborCells(sel.hexId)
+ const resolved = await Promise.all(
+ ring.map((id) => fetch(`/api/hexes/resolve?hex=${id}`).then((r) => r.json() as Promise)),
+ )
+ setNearby(resolved.filter((c) => c.status === 'curated-available' || c.status === 'global-available'))
+ }
+ } finally {
+ setStatusLoading(false)
+ }
+ }
+
+ function selectCell(sel: Selected) {
+ justSelected.current = true
+ setSelected(sel)
+ setStatus(null)
+ setHoldState('idle')
+ setQuery(sel.label)
+ setSuggestions([])
+ setOpen(false)
+ void loadStatus(sel)
+ }
+
+ function pick(s: AddressSuggestion) {
+ selectCell({ hexId: resolveContainingCell(s.lat, s.lng), label: s.label, sublabel: s.sublabel, lat: s.lat, lng: s.lng })
+ }
+
+ async function placeHold() {
+ if (!selected || !holdEmail.trim()) return
+ setHoldState('submitting')
+ try {
+ const res = await fetch('/api/holds', {
+ method: 'POST',
+ headers: { 'content-type': 'application/json' },
+ body: JSON.stringify({ email: holdEmail.trim(), lat: selected.lat, lng: selected.lng }),
+ })
+ if (!res.ok) throw new Error()
+ await loadStatus(selected) // now global-held
+ setHoldState('idle')
+ } catch {
+ setHoldState('error')
+ }
+ }
+
+ if (!token) {
+ return (
+
+
+ Add NEXT_PUBLIC_MAPBOX_TOKEN to enable address lookup.
+
+ )
+ }
+
+ const cta = status ? cellStatusCta(status.status) : null
+
+ return (
+
+
+
+
+
+
+ setQuery(e.target.value)}
+ onFocus={() => suggestions.length > 0 && setOpen(true)}
+ placeholder="Enter a street address…"
+ autoComplete="off"
+ className="w-full bg-transparent text-sm text-white placeholder-gray-600 outline-none"
+ />
+ {loading && }
+
+
+ {open && suggestions.length > 0 && (
+
+ {suggestions.map((s) => (
+ -
+
+
+ ))}
+
+ )}
+
+
+ {selected && (
+
+
+
+
+
+
{selected.label}
+
{selected.hexId}
+
+
+ {status && (
+
{formatGenesisListingUsd(status.priceUsd)}
+ )}
+
+
+
+
+ {/* Status-driven CTA */}
+
+ {statusLoading || !cta ? (
+
+ Checking availability…
+
+ ) : cta.action === 'mint' ? (
+
+ {cta.label}
+
+ ) : cta.action === 'hold' ? (
+
+
+ This cell is outside the Genesis 200. Reserve it with a free 30-day hold — you’ll get first claim when its round opens.
+
+
+ setHoldEmail(e.target.value)}
+ placeholder="you@email.com"
+ className="w-full rounded-lg border border-gray-700 bg-[#0a121f] px-3 py-2 text-sm text-white placeholder-gray-600 outline-none focus:border-malama-teal"
+ />
+
+
+ {holdState === 'error' &&
Couldn’t place the hold — try again.
}
+
+ ) : (
+
+ {status?.status === 'global-held' ? (
+ {cta.label}
+ ) : (
+ cta.label
+ )}
+
+ )}
+
+
+ {/* Nearby available cells when this one is taken */}
+ {nearby.length > 0 && (
+
+
Available nearby:
+
+ {nearby.map((n) => (
+
+ ))}
+
+
+ )}
+
+ )}
+
+ )
+}
diff --git a/apps/web/src/components/HexMap.tsx b/apps/web/src/components/HexMap.tsx
index 7472165..e1073a3 100644
--- a/apps/web/src/components/HexMap.tsx
+++ b/apps/web/src/components/HexMap.tsx
@@ -166,6 +166,35 @@ export default function HexMap() {
filter: ['==', 'id', '']
})
+ // Global holds overlay — cells reserved off-chain outside the curated 200.
+ // Rendered amber + dashed to read clearly apart from the teal Genesis grid.
+ try {
+ const holdsRes = await fetch('/api/hexes/holds')
+ const holdsData = await holdsRes.json()
+ if (Array.isArray(holdsData.features) && holdsData.features.length > 0) {
+ m.addSource('holds', { type: 'geojson', data: holdsData })
+ m.addLayer({
+ id: 'holds-fill',
+ type: 'fill',
+ source: 'holds',
+ paint: { 'fill-color': '#F59E0B', 'fill-opacity': 0.18 },
+ })
+ m.addLayer({
+ id: 'holds-line',
+ type: 'line',
+ source: 'holds',
+ paint: {
+ 'line-color': '#F59E0B',
+ 'line-width': 1.5,
+ 'line-dasharray': [2, 1.5],
+ 'line-opacity': 0.9,
+ },
+ })
+ }
+ } catch (e) {
+ console.warn('Failed to load holds overlay', e)
+ }
+
const animatePulse = () => {
if (!m.isStyleLoaded()) {
requestAnimationFrame(animatePulse)
diff --git a/apps/web/src/explorer/components/HexMap.tsx b/apps/web/src/explorer/components/HexMap.tsx
index 5afe3df..f05ae6e 100644
--- a/apps/web/src/explorer/components/HexMap.tsx
+++ b/apps/web/src/explorer/components/HexMap.tsx
@@ -65,6 +65,7 @@ export interface HexMapHandle {
const PHASE1_SOURCE = 'phase1-hexes';
const LAND_SOURCE = 'land-hexes';
const CONTEXT_SOURCE = 'context-hexes';
+const HOLDS_SOURCE = 'global-holds';
/**
* Zoom level at which the viewport-driven context hex grid becomes
@@ -389,6 +390,40 @@ export const HexMap = forwardRef(function HexMap(
m.getCanvas().style.cursor = '';
});
}
+
+ // Global holds overlay — off-chain reservations of cells outside the
+ // curated set. Amber dashed, matching the /map overlay. Added once, then
+ // populated from the holds GeoJSON endpoint.
+ if (!m.getSource(HOLDS_SOURCE)) {
+ m.addSource(HOLDS_SOURCE, {
+ type: 'geojson',
+ data: { type: 'FeatureCollection', features: [] },
+ });
+ m.addLayer({
+ id: 'holds-fill',
+ type: 'fill',
+ source: HOLDS_SOURCE,
+ paint: { 'fill-color': '#F59E0B', 'fill-opacity': 0.18 },
+ });
+ m.addLayer({
+ id: 'holds-line',
+ type: 'line',
+ source: HOLDS_SOURCE,
+ paint: {
+ 'line-color': '#F59E0B',
+ 'line-width': 1.5,
+ 'line-dasharray': [2, 1.5],
+ 'line-opacity': 0.9,
+ },
+ });
+ fetch('/api/hexes/holds')
+ .then((r) => r.json())
+ .then((data) => {
+ const src = m.getSource(HOLDS_SOURCE) as mapboxgl.GeoJSONSource | undefined;
+ if (src && Array.isArray(data.features)) src.setData(data);
+ })
+ .catch((e) => console.warn('Failed to load holds overlay', e));
+ }
}
}, [currentResolution, landCells, manifest, onHexClick]);
diff --git a/apps/web/src/lib/cell-status.test.ts b/apps/web/src/lib/cell-status.test.ts
new file mode 100644
index 0000000..9084248
--- /dev/null
+++ b/apps/web/src/lib/cell-status.test.ts
@@ -0,0 +1,59 @@
+import { describe, it, expect } from 'vitest'
+import { classifyCellStatus, resolveCellStatus, cellStatusCta } from './cell-status'
+
+describe('classifyCellStatus', () => {
+ it('marks an unclaimed curated cell as curated-available', () => {
+ expect(classifyCellStatus({ isNative: false, isCurated: true, isClaimed: false, isHeld: false }))
+ .toBe('curated-available')
+ })
+
+ it('marks a claimed curated cell as curated-taken', () => {
+ expect(classifyCellStatus({ isNative: false, isCurated: true, isClaimed: true, isHeld: false }))
+ .toBe('curated-taken')
+ })
+
+ it('marks an unheld non-curated cell as global-available', () => {
+ expect(classifyCellStatus({ isNative: false, isCurated: false, isClaimed: false, isHeld: false }))
+ .toBe('global-available')
+ })
+
+ it('marks a held non-curated cell as global-held', () => {
+ expect(classifyCellStatus({ isNative: false, isCurated: false, isClaimed: false, isHeld: true }))
+ .toBe('global-held')
+ })
+
+ it('treats native cells as native regardless of any other flag', () => {
+ expect(classifyCellStatus({ isNative: true, isCurated: true, isClaimed: true, isHeld: true }))
+ .toBe('native')
+ })
+
+ it('rejects an invalid H3 cell instead of returning garbage', async () => {
+ await expect(resolveCellStatus('not-a-hex')).rejects.toThrow()
+ })
+})
+
+describe('cellStatusCta', () => {
+ it('offers minting for an available curated cell', () => {
+ const cta = cellStatusCta('curated-available')
+ expect(cta.action).toBe('mint')
+ expect(cta.enabled).toBe(true)
+ })
+
+ it('offers a hold for an available global cell', () => {
+ const cta = cellStatusCta('global-available')
+ expect(cta.action).toBe('hold')
+ expect(cta.enabled).toBe(true)
+ })
+
+ it('disables taken and held cells', () => {
+ expect(cellStatusCta('curated-taken').enabled).toBe(false)
+ expect(cellStatusCta('curated-taken').action).toBe('none')
+ expect(cellStatusCta('global-held').enabled).toBe(false)
+ })
+
+ it('disables native cells with a tribal-reservation label', () => {
+ const cta = cellStatusCta('native')
+ expect(cta.enabled).toBe(false)
+ expect(cta.label).toMatch(/tribe|native|reserved/i)
+ })
+})
diff --git a/apps/web/src/lib/cell-status.ts b/apps/web/src/lib/cell-status.ts
new file mode 100644
index 0000000..8b88198
--- /dev/null
+++ b/apps/web/src/lib/cell-status.ts
@@ -0,0 +1,93 @@
+/**
+ * Cell status resolution for the address-lookup flow.
+ *
+ * Pure `classifyCellStatus` decides the status from already-gathered facts (TDD-covered);
+ * async `resolveCellStatus` gathers those facts (curated membership, claim, hold, native)
+ * and adds price + centroid for the UI.
+ */
+
+import { cellToLatLng, getResolution, isValidCell } from 'h3-js'
+import { calculateGenesisListingPriceDeterministic } from '@/lib/h3'
+import { getClaimByHex } from '@/lib/genesis-claim-registry'
+import { getGlobalHold } from '@/lib/global-hold-store'
+import regionsData from '@/data/regions.json'
+import nativeHexesData from '@/data/genesis-native-hexes.json'
+
+export type CellStatus =
+ | 'native'
+ | 'curated-available'
+ | 'curated-taken'
+ | 'global-available'
+ | 'global-held'
+
+export function classifyCellStatus(facts: {
+ isNative: boolean
+ isCurated: boolean
+ isClaimed: boolean
+ isHeld: boolean
+}): CellStatus {
+ if (facts.isNative) return 'native'
+ if (facts.isCurated) return facts.isClaimed ? 'curated-taken' : 'curated-available'
+ return facts.isHeld ? 'global-held' : 'global-available'
+}
+
+export type CellCta = {
+ label: string
+ action: 'mint' | 'hold' | 'none'
+ enabled: boolean
+ tone: 'primary' | 'muted'
+}
+
+/** Map a cell status to the call-to-action the lookup UI should render. */
+export function cellStatusCta(status: CellStatus): CellCta {
+ switch (status) {
+ case 'curated-available':
+ return { label: 'Reserve & mint this hex', action: 'mint', enabled: true, tone: 'primary' }
+ case 'global-available':
+ return { label: 'Reserve this cell (30-day hold)', action: 'hold', enabled: true, tone: 'primary' }
+ case 'curated-taken':
+ return { label: 'Already claimed', action: 'none', enabled: false, tone: 'muted' }
+ case 'global-held':
+ return { label: 'Already reserved', action: 'none', enabled: false, tone: 'muted' }
+ case 'native':
+ return { label: 'Reserved for Native Tribes', action: 'none', enabled: false, tone: 'muted' }
+ }
+}
+
+// Built once at module load — the curated 200 and native sets are static data.
+const CURATED_HEX_SET = new Set(
+ Object.values(regionsData as Record).flatMap((r) => r?.cells ?? []),
+)
+const NATIVE_HEX_SET = new Set(Object.keys(nativeHexesData as Record))
+
+export type CellStatusResult = {
+ hexId: string
+ resolution: number
+ status: CellStatus
+ lat: number
+ lng: number
+ priceUsd: number
+}
+
+export async function resolveCellStatus(hexId: string): Promise {
+ if (!isValidCell(hexId)) throw new Error(`Invalid H3 cell: ${hexId}`)
+ const [lat, lng] = cellToLatLng(hexId)
+
+ const [claim, hold] = await Promise.all([getClaimByHex(hexId), getGlobalHold(hexId)])
+
+ const status = classifyCellStatus({
+ isNative: NATIVE_HEX_SET.has(hexId),
+ isCurated: CURATED_HEX_SET.has(hexId),
+ isClaimed: !!claim,
+ isHeld: !!hold,
+ })
+
+ return {
+ hexId,
+ resolution: getResolution(hexId),
+ status,
+ lat,
+ lng,
+ priceUsd: calculateGenesisListingPriceDeterministic(lat, lng, hexId),
+ }
+}
diff --git a/apps/web/src/lib/global-hold-store.test.ts b/apps/web/src/lib/global-hold-store.test.ts
new file mode 100644
index 0000000..52ca678
--- /dev/null
+++ b/apps/web/src/lib/global-hold-store.test.ts
@@ -0,0 +1,96 @@
+import { describe, it, expect } from 'vitest'
+import { issueGlobalHold, getGlobalHold, releaseGlobalHold, listHoldsByEmail, listActiveHolds } from './global-hold-store'
+import { getStats } from './genesis-claim-registry'
+
+// Distinct global Res-4 cells per test — memKv persists in-process between tests,
+// so each test owns a unique cell to stay isolated.
+const BERLIN = '841f1d5ffffffff'
+
+describe('global-hold-store', () => {
+ it('places an exclusive hold on a global Res-4 cell and reads it back', async () => {
+ const res = await issueGlobalHold({ hexId: BERLIN, email: 'a@example.com', lat: 52.5163, lng: 13.3777 })
+
+ expect(res.ok).toBe(true)
+
+ const hold = await getGlobalHold(BERLIN)
+ expect(hold?.hexId).toBe(BERLIN)
+ expect(hold?.email).toBe('a@example.com')
+ expect(hold?.status).toBe('held')
+ expect(hold?.resolution).toBe(4)
+ })
+
+ it('rejects a second hold on a cell already held by someone else', async () => {
+ const NYC = '842a101ffffffff'
+ const first = await issueGlobalHold({ hexId: NYC, email: 'a@example.com', lat: 40.7484, lng: -73.9857 })
+ expect(first.ok).toBe(true)
+
+ const second = await issueGlobalHold({ hexId: NYC, email: 'b@example.com', lat: 40.7484, lng: -73.9857 })
+ expect(second.ok).toBe(false)
+ if (!second.ok) {
+ expect(second.error).toMatch(/already held/i)
+ expect(second.existing?.email).toBe('a@example.com')
+ }
+ })
+
+ it('lets a cell be re-held by someone else after the hold expires', async () => {
+ const HON = '84464b9ffffffff'
+ const t0 = Date.parse('2026-01-01T00:00:00Z')
+
+ const first = await issueGlobalHold({ hexId: HON, email: 'a@example.com', lat: 21.3, lng: -157.8 }, { now: t0 })
+ expect(first.ok).toBe(true)
+
+ const past = t0 + 31 * 24 * 60 * 60 * 1000 // 31 days later — past the 30-day TTL
+ expect(await getGlobalHold(HON, { now: past })).toBeNull()
+
+ const reheld = await issueGlobalHold({ hexId: HON, email: 'b@example.com', lat: 21.3, lng: -157.8 }, { now: past })
+ expect(reheld.ok).toBe(true)
+ expect((await getGlobalHold(HON, { now: past }))?.email).toBe('b@example.com')
+ })
+
+ it('never consumes a Genesis edition — the 200-cap counter is untouched', async () => {
+ const before = await getStats()
+ await issueGlobalHold({ hexId: '842a107ffffffff', email: 'iso@example.com', lat: 40.67, lng: -73.98 })
+ const after = await getStats()
+
+ expect(after.issued).toBe(before.issued)
+ expect(after.issued).toBe(0)
+ })
+
+ it('lets the owner release a hold (and refuses non-owners), freeing the cell', async () => {
+ const LDN = '84194adffffffff'
+ await issueGlobalHold({ hexId: LDN, email: 'owner@example.com', lat: 51.5, lng: -0.12 })
+
+ const denied = await releaseGlobalHold(LDN, 'intruder@example.com')
+ expect(denied.ok).toBe(false)
+ expect(await getGlobalHold(LDN)).not.toBeNull()
+
+ const released = await releaseGlobalHold(LDN, 'owner@example.com')
+ expect(released.ok).toBe(true)
+ expect(await getGlobalHold(LDN)).toBeNull()
+
+ const reheld = await issueGlobalHold({ hexId: LDN, email: 'new@example.com', lat: 51.5, lng: -0.12 })
+ expect(reheld.ok).toBe(true)
+ })
+
+ it('lists a user’s active holds', async () => {
+ const TYO = '842f5a3ffffffff'
+ const SF = '8428309ffffffff'
+ await issueGlobalHold({ hexId: TYO, email: 'multi@example.com', lat: 35.6, lng: 139.6 })
+ await issueGlobalHold({ hexId: SF, email: 'multi@example.com', lat: 37.7, lng: -122.4 })
+
+ const holds = await listHoldsByEmail('multi@example.com')
+ expect(holds.map((h) => h.hexId).sort()).toEqual([SF, TYO].sort())
+ expect(holds.every((h) => h.email === 'multi@example.com')).toBe(true)
+ })
+
+ it('lists all active holds across users (for the map overlay)', async () => {
+ const SYD = '84be0e3ffffffff'
+ const CAI = '843e629ffffffff'
+ await issueGlobalHold({ hexId: SYD, email: 'au@example.com', lat: -33.87, lng: 151.21 })
+ await issueGlobalHold({ hexId: CAI, email: 'eg@example.com', lat: 30.04, lng: 31.24 })
+
+ const ids = (await listActiveHolds()).map((h) => h.hexId)
+ expect(ids).toContain(SYD)
+ expect(ids).toContain(CAI)
+ })
+})
diff --git a/apps/web/src/lib/global-hold-store.ts b/apps/web/src/lib/global-hold-store.ts
new file mode 100644
index 0000000..5493802
--- /dev/null
+++ b/apps/web/src/lib/global-hold-store.ts
@@ -0,0 +1,145 @@
+/**
+ * KV-backed store for GLOBAL hex holds — off-chain, exclusive reservations of
+ * Res-4 cells OUTSIDE the curated Genesis 200. These never mint and never consume
+ * a Genesis edition; they live in their own `hold:*` namespace so the live mint
+ * path (`genesis:*`) is completely untouched.
+ *
+ * Atomicity mirrors genesis-claim-registry: sadd(K.held, hexId) is a single Redis
+ * command (1 on first insert, 0 if already held), making holds race-safe.
+ *
+ * Every hold is keyed by its canonical Res-4 H3 index + owner email, so a future
+ * global resolution change can fractionalize the cell via cellToChildren().
+ */
+
+import { kv } from '@/lib/kv'
+import { RES4 } from '@/lib/hex-lookup'
+
+export const HOLD_TTL_DAYS = 30
+const HOLD_TTL_MS = HOLD_TTL_DAYS * 24 * 60 * 60 * 1000
+
+export type GlobalHold = {
+ hexId: string
+ resolution: typeof RES4
+ email: string
+ lat: number
+ lng: number
+ status: 'held'
+ heldAt: string
+ expiresAt: string
+ referrerId?: string
+}
+
+// ── KV key schema (separate `hold:*` namespace — never touches `genesis:*`) ──────
+// hold:held → Set (atomic exclusivity via sadd)
+// hold:detail: → GlobalHold
+// hold:email: → Set
+// hold:index → Set (all holds, for the map overlay)
+const K = {
+ held: 'hold:held',
+ index: 'hold:index',
+ detail: (hexId: string) => `hold:detail:${hexId}`,
+ email: (email: string) => `hold:email:${email}`,
+}
+
+export type IssueHoldInput = {
+ hexId: string
+ email: string
+ lat: number
+ lng: number
+ referrerId?: string
+}
+
+/** `now` is injectable so expiry is deterministically testable; defaults to wall clock. */
+type ClockOpts = { now?: number }
+
+/** Read a hold only if it hasn't expired as of `now`. Pure read, no mutation. */
+async function readLiveHold(hexId: string, now: number): Promise {
+ const hold = await kv.get(K.detail(hexId))
+ if (!hold) return null
+ return Date.parse(hold.expiresAt) <= now ? null : hold
+}
+
+export async function issueGlobalHold(
+ input: IssueHoldInput,
+ opts?: ClockOpts,
+): Promise<{ ok: true; hold: GlobalHold } | { ok: false; error: string; existing?: GlobalHold }> {
+ const now = opts?.now ?? Date.now()
+
+ // sadd is atomic: 0 means the cell was already in the held-set.
+ const added = await kv.sadd(K.held, input.hexId)
+ if (added === 0) {
+ const live = await readLiveHold(input.hexId, now)
+ if (live) return { ok: false, error: 'Cell already held', existing: live }
+ // Expired or orphaned — reclaim in place (the set member is already present).
+ }
+
+ const hold: GlobalHold = {
+ hexId: input.hexId,
+ resolution: RES4,
+ email: input.email,
+ lat: input.lat,
+ lng: input.lng,
+ status: 'held',
+ heldAt: new Date(now).toISOString(),
+ expiresAt: new Date(now + HOLD_TTL_MS).toISOString(),
+ referrerId: input.referrerId,
+ }
+
+ await Promise.all([
+ kv.set(K.detail(input.hexId), hold),
+ kv.sadd(K.email(input.email), input.hexId),
+ kv.sadd(K.index, input.hexId),
+ ])
+
+ return { ok: true, hold }
+}
+
+/** All of a user's currently-active holds (expired ones are filtered and cleaned). */
+export async function listHoldsByEmail(email: string, opts?: ClockOpts): Promise {
+ const ids = await kv.smembers(K.email(email))
+ const holds = await Promise.all(ids.map((id) => getGlobalHold(id, opts)))
+ return holds.filter((h): h is GlobalHold => h !== null)
+}
+
+/** Every currently-active hold across all users (for the map overlay). */
+export async function listActiveHolds(opts?: ClockOpts): Promise {
+ const ids = await kv.smembers(K.index)
+ const holds = await Promise.all(ids.map((id) => getGlobalHold(id, opts)))
+ return holds.filter((h): h is GlobalHold => h !== null)
+}
+
+/** Release a hold. Only the owning email may release it. */
+export async function releaseGlobalHold(
+ hexId: string,
+ email: string,
+): Promise<{ ok: boolean; reason?: string }> {
+ const existing = await kv.get(K.detail(hexId))
+ if (existing && existing.email !== email) {
+ return { ok: false, reason: 'Only the holder can release this cell' }
+ }
+ await Promise.all([
+ kv.srem(K.held, hexId),
+ kv.del(K.detail(hexId)),
+ kv.srem(K.index, hexId),
+ ...(existing ? [kv.srem(K.email(existing.email), hexId)] : []),
+ ])
+ return { ok: true }
+}
+
+export async function getGlobalHold(hexId: string, opts?: ClockOpts): Promise {
+ const now = opts?.now ?? Date.now()
+ const live = await readLiveHold(hexId, now)
+ if (live) return live
+
+ // Expired record lingering in KV — lazily clean up every index so the cell frees.
+ const stale = await kv.get(K.detail(hexId))
+ if (stale) {
+ await Promise.all([
+ kv.srem(K.held, hexId),
+ kv.del(K.detail(hexId)),
+ kv.srem(K.email(stale.email), hexId),
+ kv.srem(K.index, hexId),
+ ])
+ }
+ return null
+}
diff --git a/apps/web/src/lib/hex-lookup.test.ts b/apps/web/src/lib/hex-lookup.test.ts
new file mode 100644
index 0000000..fb0d45d
--- /dev/null
+++ b/apps/web/src/lib/hex-lookup.test.ts
@@ -0,0 +1,41 @@
+import { describe, it, expect } from 'vitest'
+import { getResolution } from 'h3-js'
+import { resolveContainingCell, neighborCells } from './hex-lookup'
+
+describe('resolveContainingCell', () => {
+ it('returns the Res-4 cell that contains a coordinate', () => {
+ // Los Angeles City Hall — its containing Res-4 cell is the Genesis West-Coast lab node.
+ const cell = resolveContainingCell(34.0537, -118.2428)
+
+ expect(cell).toBe('8429a1dffffffff')
+ expect(getResolution(cell)).toBe(4)
+ })
+
+ it('resolves coordinates anywhere on Earth, not just the curated US set', () => {
+ // Brandenburg Gate, Berlin — outside the curated 200, must still resolve.
+ const cell = resolveContainingCell(52.5163, 13.3777)
+
+ expect(cell).toBe('841f1d5ffffffff')
+ expect(getResolution(cell)).toBe(4)
+ })
+
+ it('maps two addresses in the same vicinity to the same cell, and a distant one to a different cell', () => {
+ const cityHall = resolveContainingCell(34.0537, -118.2428)
+ const nearby = resolveContainingCell(34.056, -118.245) // ~500m away
+ const distant = resolveContainingCell(52.5163, 13.3777) // Berlin
+
+ expect(nearby).toBe(cityHall)
+ expect(distant).not.toBe(cityHall)
+ })
+})
+
+describe('neighborCells', () => {
+ it('returns the surrounding cells, excluding the cell itself', () => {
+ const cell = '8429a1dffffffff'
+ const neighbors = neighborCells(cell)
+
+ expect(neighbors).toHaveLength(6)
+ expect(neighbors).not.toContain(cell)
+ expect(neighbors.every((id) => getResolution(id) === 4)).toBe(true)
+ })
+})
diff --git a/apps/web/src/lib/hex-lookup.ts b/apps/web/src/lib/hex-lookup.ts
new file mode 100644
index 0000000..2d9efe0
--- /dev/null
+++ b/apps/web/src/lib/hex-lookup.ts
@@ -0,0 +1,14 @@
+import { latLngToCell, gridDisk } from 'h3-js'
+
+/** The H3 resolution sold in the current funding round (~1,770 km² per cell). */
+export const RES4 = 4
+
+/** Resolve the canonical Res-4 H3 cell that contains the given coordinate. */
+export function resolveContainingCell(lat: number, lng: number): string {
+ return latLngToCell(lat, lng, RES4)
+}
+
+/** The ring of cells immediately surrounding a cell (excludes the cell itself). */
+export function neighborCells(hexId: string): string[] {
+ return gridDisk(hexId, 1).filter((id) => id !== hexId)
+}
diff --git a/apps/web/src/lib/mapbox-geocode.test.ts b/apps/web/src/lib/mapbox-geocode.test.ts
new file mode 100644
index 0000000..1e67659
--- /dev/null
+++ b/apps/web/src/lib/mapbox-geocode.test.ts
@@ -0,0 +1,39 @@
+import { describe, it, expect } from 'vitest'
+import { parseForwardSuggestions } from './mapbox-geocode'
+
+// Shape of a Mapbox Search API v6 forward-geocode feature.
+const feature = (over: Record = {}) => ({
+ geometry: { type: 'Point', coordinates: [-118.2428, 34.0537] },
+ properties: {
+ mapbox_id: 'addr.123',
+ name: '200 N Spring St',
+ full_address: '200 N Spring St, Los Angeles, California 90012, United States',
+ place_formatted: 'Los Angeles, California 90012, United States',
+ ...over,
+ },
+})
+
+describe('parseForwardSuggestions', () => {
+ it('maps forward-geocode features to selectable suggestions with coordinates', () => {
+ const [s] = parseForwardSuggestions({ features: [feature()] })
+
+ expect(s.id).toBe('addr.123')
+ expect(s.label).toBe('200 N Spring St')
+ expect(s.sublabel).toBe('Los Angeles, California 90012, United States')
+ expect(s.lat).toBeCloseTo(34.0537)
+ expect(s.lng).toBeCloseTo(-118.2428)
+ })
+
+ it('drops features without point coordinates', () => {
+ const broken = { geometry: null, properties: { mapbox_id: 'x', name: 'No geo' } }
+ const result = parseForwardSuggestions({ features: [broken, feature()] })
+
+ expect(result).toHaveLength(1)
+ expect(result[0].id).toBe('addr.123')
+ })
+
+ it('returns an empty list when there are no features', () => {
+ expect(parseForwardSuggestions({})).toEqual([])
+ expect(parseForwardSuggestions({ features: [] })).toEqual([])
+ })
+})
diff --git a/apps/web/src/lib/mapbox-geocode.ts b/apps/web/src/lib/mapbox-geocode.ts
new file mode 100644
index 0000000..27f85ec
--- /dev/null
+++ b/apps/web/src/lib/mapbox-geocode.ts
@@ -0,0 +1,47 @@
+/**
+ * Parsing for the Mapbox Search API v6 forward-geocode response.
+ * Network I/O lives in the component; this module only shapes the response
+ * into selectable address suggestions, so it can be unit-tested in isolation.
+ */
+
+export type AddressSuggestion = {
+ /** Mapbox feature id (stable key for the list). */
+ id: string
+ /** Primary line, e.g. the street address or place name. */
+ label: string
+ /** Secondary line, e.g. "Los Angeles, California 90012, United States". */
+ sublabel: string
+ lat: number
+ lng: number
+}
+
+type V6ForwardFeature = {
+ geometry?: { type?: string; coordinates?: number[] } | null
+ properties?: {
+ mapbox_id?: string
+ name?: string
+ full_address?: string
+ place_formatted?: string
+ }
+}
+
+export function parseForwardSuggestions(data: { features?: V6ForwardFeature[] }): AddressSuggestion[] {
+ const features = data?.features ?? []
+ const out: AddressSuggestion[] = []
+
+ for (const f of features) {
+ const coords = f.geometry?.coordinates
+ if (!coords || coords.length < 2) continue
+
+ const props = f.properties ?? {}
+ out.push({
+ id: props.mapbox_id || props.full_address || props.name || `${coords[1]},${coords[0]}`,
+ label: props.name || props.full_address || 'Unknown location',
+ sublabel: props.place_formatted || props.full_address || '',
+ lng: coords[0],
+ lat: coords[1],
+ })
+ }
+
+ return out
+}
diff --git a/apps/web/vitest.config.ts b/apps/web/vitest.config.ts
new file mode 100644
index 0000000..3bbb4db
--- /dev/null
+++ b/apps/web/vitest.config.ts
@@ -0,0 +1,14 @@
+import { defineConfig } from 'vitest/config'
+import { fileURLToPath } from 'node:url'
+
+export default defineConfig({
+ resolve: {
+ alias: {
+ '@': fileURLToPath(new URL('./src', import.meta.url)),
+ },
+ },
+ test: {
+ environment: 'node',
+ include: ['src/**/*.test.ts'],
+ },
+})
diff --git a/docs/prd/address-hex-lookup.md b/docs/prd/address-hex-lookup.md
new file mode 100644
index 0000000..84842bc
--- /dev/null
+++ b/docs/prd/address-hex-lookup.md
@@ -0,0 +1,120 @@
+# PRD: Address → Hex Lookup & Reserve (Res 4)
+
+Status: Draft · Owner: Tyler · App: `apps/web` (launch.malamalabs.com)
+
+## Context
+
+Buyers should be able to type their address, see the exact H3 cell that contains it, and
+reserve it to purchase. The current presale lets people claim from a **curated set of 200
+Res-4 cells** (US-only, `regions.json`) selected on a Mapbox view — there is no way to look up
+an arbitrary address and act on the cell that contains it.
+
+We already run real Uber H3 (`h3-js@4.1.0`) and a race-safe reservation/mint stack. The
+genuinely new piece is **forward** address lookup (address → lat/lng → containing cell) and a
+path for cells **outside** the curated 200.
+
+This is step one toward the larger goal: **a Res-4 owner can later fractionalize their cell
+into its inner H3 children and resell them when the protocol makes a global resolution change
+(Res 4 → 5 → 6 → 7).** Nothing built here may preclude that.
+
+## Goal
+
+Let a user enter any address worldwide, see the **Res-4** cell (1,770 km²) containing it, and:
+- **If the cell is one of the curated 200 and available** → reserve + mint through the existing flow.
+- **If the cell is outside the 200 (anywhere on Earth)** → place an off-chain **global hold**
+ (exclusive, free, email-anchored, 30-day expiry). No mint yet.
+
+## Non-goals (explicitly out of scope for this iteration)
+
+- No changes to the deployed `GenesisValidator.sol` contract. It is **frozen**.
+- No fractionalization / secondary resale of inner hexes (future iteration — but the data model must support it).
+- No new funding-round pricing tiers (Res 5/6/7, $2,500–$3,500). Current round is **Res 4 only**.
+- No payment/deposit for global holds (they cannot mint, so they are free interest+lock).
+
+## Core model
+
+| Address resolves to a Res-4 cell that is… | Behavior |
+|---|---|
+| In curated 200, available | **Reserve + mint** — existing `issueClaim` → contract `secureNode`/`adminSecureNode`. Unchanged. |
+| Outside curated 200 (global) | **Global hold** — new KV store, separate namespace, no edition consumed, no mint. |
+| Already claimed (200) or already held (global) | "Taken" — show owner-less status, offer nearby alternatives. |
+| Native/tribal (`NATIVE_HEX_SET`) | Blocked, as today. |
+
+**Invariants**
+- The 200 Genesis editions and the on-chain `MAX_GENESIS_SUPPLY = 200` cap are untouched.
+- Global holds live in their **own** KV namespace — never increment `genesis:issued`.
+- Every mint **and** every hold is stored keyed by its **canonical Res-4 H3 index + owner**,
+ so a future global res change enables `cellToChildren(res4Index, n)` → fractionalize/resell.
+
+## User flow
+
+1. User opens the address-lookup surface (new component, e.g. `AddressHexLookup.tsx`).
+2. Types an address → **Mapbox forward geocoding** autocomplete (Search Box / Geocoding v6
+ forward; reuses `NEXT_PUBLIC_MAPBOX_TOKEN`). Picks a result → `{ lat, lng }`.
+3. `latLngToCell(lat, lng, 4)` → canonical Res-4 index. Map flies to it; `cellToBoundary` draws it.
+4. Status resolved via a single endpoint (curated-available / curated-taken / global-available /
+ global-held / native). Price via existing `calculateGenesisListingPriceDeterministic` (already global).
+5. CTA depends on status:
+ - curated-available → enter existing presale/checkout with `?hex=`.
+ - global-available → "Reserve this cell" → email → creates an exclusive global hold.
+ - taken/native → disabled + nearby suggestions (`gridDisk(index, 1)` filtered to available).
+
+## Data model — new `global-hold-store.ts` (KV, mirrors `genesis-claim-registry.ts`)
+
+```
+GlobalHold {
+ hexId: string // canonical Res-4 H3 index (the fractionalization anchor)
+ resolution: 4
+ email: string // owner identity (reuses email session)
+ lat, lng: number // geocoded centroid of the address (for display)
+ status: 'held'
+ heldAt: ISO // created
+ expiresAt: ISO // heldAt + 30d
+ referrerId?: string // KOL attribution, same as claims
+}
+
+KV keys (separate namespace — never touches genesis:*):
+ hold:res4: -> GlobalHold (exclusive: setNX / atomic guard)
+ hold:email: -> Set (a user's holds)
+ hold:index -> Set (all active holds, for map overlay)
+```
+
+Exclusivity via atomic set-if-absent (mirror the `sadd`/lock pattern in
+`genesis-claim-registry.ts` / `custodial-store.ts`). Expiry via stored `expiresAt` +
+lazy reclaim on read (same idea as the 20-min orphaned-claim sweep).
+
+## Reuse (do not rebuild)
+
+- `h3-js`: `latLngToCell`, `cellToBoundary`, `cellToLatLng`, `gridDisk`, `cellToChildren` (future).
+- Pricing: `calculateGenesisListingPriceDeterministic(lat, lng, hexId)` in `lib/h3.ts` — already global.
+- Curated availability: `genesis-claim-registry.ts`, `regions.json`, `NATIVE_HEX_SET`.
+- Map: `HexBoundaryPreview.tsx` (already renders a single cell + reverse-geocodes), `explorer` H3 utils.
+- Checkout: existing presale/`create-session` for curated cells (relax nothing on-chain).
+
+## Vertical slices (candidate issues)
+
+1. **Forward geocode + resolve cell (read-only):** address autocomplete → `latLngToCell(…,4)` →
+ render the containing cell on the map. No reservation. Ships standalone value.
+2. **Cell status endpoint:** `GET /api/hexes/resolve?hex=` → curated-available |
+ curated-taken | global-available | global-held | native, + price + centroid.
+3. **Global hold store + API:** `global-hold-store.ts` + `POST/GET/DELETE /api/holds` (exclusive,
+ email-anchored, 30-day expiry). Separate KV namespace; unit-tested for the race + cap-isolation.
+4. **Lookup UI wiring:** `AddressHexLookup.tsx` — status-driven CTA (mint route vs hold vs taken),
+ nearby suggestions via `gridDisk`.
+5. **Global holds on the map (optional):** overlay active holds as a distinct layer on `/map` / `/explorer`.
+
+## Risks / edge cases
+
+- Address geocodes to ocean/unpopulated cell → allow (it's still a valid Res-4 index) but flag water via existing `hex-geo` sampling.
+- Curated cell membership is currently checked only in `create-session` — confirm we don't accidentally let a global cell enter the *mint* path (it must route to holds, not `issueClaim`).
+- Resolution drift: assert `getResolution(index) === 4` everywhere a Res-4 index is expected.
+- Hold → future mint conversion is out of scope, but store enough (`hexId`, `email`, `referrerId`) to convert later without data loss.
+
+## Verification
+
+- Unit: `global-hold-store` — exclusive lock under concurrent holds; expiry reclaim; never touches `genesis:issued`.
+- Integration: resolve endpoint returns correct status for (a) a known curated cell, (b) a known
+ taken cell, (c) an arbitrary global address (e.g. Berlin), (d) a native cell.
+- Manual (run the app): type a US curated address → reserve+mint path; type an overseas address →
+ global hold path; re-enter the same overseas address from another email → "taken."
+- Regression: existing presale/mint flow for the curated 200 is byte-for-byte unchanged.
diff --git a/package-lock.json b/package-lock.json
index 9ce9a14..2249d37 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -50,7 +50,8 @@
"autoprefixer": "^10.4.19",
"postcss": "^8.4.38",
"tailwindcss": "^3.4.3",
- "typescript": "^5"
+ "typescript": "^5",
+ "vitest": "^4.1.9"
}
},
"apps/web/node_modules/@types/node": {
@@ -8616,6 +8617,46 @@
"node": "^20.19.0 || >=22.12.0"
}
},
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.2.tgz",
+ "integrity": "sha512-X/7bVLWelEsbyWDUSXt7zVsTniLLPIY2n1rH58qr78l9i7MNbbxBWD8gI2vRfBWf4NUXJCUuQnfZDsp32LqsfQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.2.tgz",
+ "integrity": "sha512-gb6dYKW/1KDorGXyy48glEBJs/sxVSC5pcVrox/pFGV4mvwSFeg2sK5L2tRkVsVlh7kueqOgg4GEcuipJcGuKg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
"node_modules/@rolldown/binding-linux-x64-gnu": {
"version": "1.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.1.tgz",
@@ -10066,6 +10107,13 @@
"antlr4ts": "^0.5.0-alpha.4"
}
},
+ "node_modules/@standard-schema/spec": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@stricahq/bip32ed25519": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@stricahq/bip32ed25519/-/bip32ed25519-1.1.2.tgz",
@@ -12676,6 +12724,13 @@
"@types/ms": "*"
}
},
+ "node_modules/@types/deep-eql": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/docker-modem": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz",
@@ -12791,14 +12846,20 @@
"license": "MIT"
},
"node_modules/@types/node": {
- "version": "20.11.0",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.0.tgz",
- "integrity": "sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxAjqx41zp9qlIAVFi0IhCNsJcXolEqLWhbFbEeL0PvYm4pcQ==",
+ "version": "26.0.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.0.tgz",
+ "integrity": "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA==",
"license": "MIT",
"dependencies": {
- "undici-types": "~5.26.4"
+ "undici-types": "~8.3.0"
}
},
+ "node_modules/@types/node/node_modules/undici-types": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
+ "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
+ "license": "MIT"
+ },
"node_modules/@types/pbf": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.5.tgz",
@@ -13261,6 +13322,16 @@
"undici": "5.28.4"
}
},
+ "node_modules/@vercel/node/node_modules/@types/node": {
+ "version": "20.11.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.0.tgz",
+ "integrity": "sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxAjqx41zp9qlIAVFi0IhCNsJcXolEqLWhbFbEeL0PvYm4pcQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~5.26.4"
+ }
+ },
"node_modules/@vercel/node/node_modules/async-listen": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/async-listen/-/async-listen-3.0.0.tgz",
@@ -13591,6 +13662,160 @@
"ts-morph": "12.0.0"
}
},
+ "node_modules/@vitest/expect": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz",
+ "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.1.0",
+ "@types/chai": "^5.2.2",
+ "@vitest/spy": "4.1.9",
+ "@vitest/utils": "4.1.9",
+ "chai": "^6.2.2",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/expect/node_modules/@types/chai": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/deep-eql": "*",
+ "assertion-error": "^2.0.1"
+ }
+ },
+ "node_modules/@vitest/expect/node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@vitest/expect/node_modules/chai": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
+ "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz",
+ "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "4.1.9",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.21"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/mocker/node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz",
+ "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz",
+ "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "4.1.9",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz",
+ "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.9",
+ "@vitest/utils": "4.1.9",
+ "magic-string": "^0.30.21",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz",
+ "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz",
+ "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.9",
+ "convert-source-map": "^2.0.0",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
"node_modules/@wagmi/connectors": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/@wagmi/connectors/-/connectors-6.2.0.tgz",
@@ -17461,8 +17686,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/cookie": {
"version": "0.4.2",
@@ -19514,6 +19738,16 @@
"node": ">=6"
}
},
+ "node_modules/expect-type": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
+ "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"node_modules/extension-port-stream": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/extension-port-stream/-/extension-port-stream-3.0.0.tgz",
@@ -22109,63 +22343,336 @@
"immediate": "~3.0.5"
}
},
- "node_modules/lilconfig": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
- "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
- "license": "MIT",
+ "node_modules/lightningcss": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
"engines": {
- "node": ">=14"
+ "node": ">= 12.0.0"
},
"funding": {
- "url": "https://github.com/sponsors/antonk52"
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.32.0",
+ "lightningcss-darwin-arm64": "1.32.0",
+ "lightningcss-darwin-x64": "1.32.0",
+ "lightningcss-freebsd-x64": "1.32.0",
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
+ "lightningcss-linux-arm64-gnu": "1.32.0",
+ "lightningcss-linux-arm64-musl": "1.32.0",
+ "lightningcss-linux-x64-gnu": "1.32.0",
+ "lightningcss-linux-x64-musl": "1.32.0",
+ "lightningcss-win32-arm64-msvc": "1.32.0",
+ "lightningcss-win32-x64-msvc": "1.32.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
}
},
- "node_modules/limiter": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz",
- "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA=="
- },
- "node_modules/lines-and-columns": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
- "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
- "license": "MIT"
- },
- "node_modules/lit": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.0.tgz",
- "integrity": "sha512-DGVsqsOIHBww2DqnuZzW7QsuCdahp50ojuDaBPC7jUDRpYoH0z7kHBBYZewRzer75FwtrkmkKk7iOAwSaWdBmw==",
- "license": "BSD-3-Clause",
- "dependencies": {
- "@lit/reactive-element": "^2.1.0",
- "lit-element": "^4.2.0",
- "lit-html": "^3.3.0"
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
}
},
- "node_modules/lit-element": {
- "version": "4.2.2",
- "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.2.tgz",
- "integrity": "sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==",
- "license": "BSD-3-Clause",
- "dependencies": {
- "@lit-labs/ssr-dom-shim": "^1.5.0",
- "@lit/reactive-element": "^2.1.0",
- "lit-html": "^3.3.0"
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
}
},
- "node_modules/lit-html": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.2.tgz",
- "integrity": "sha512-Qy9hU88zcmaxBXcc10ZpdK7cOLXvXpRoBxERdtqV9QOrfpMZZ6pSYP91LhpPtap3sFMUiL7Tw2RImbe0Al2/kw==",
- "license": "BSD-3-Clause",
- "dependencies": {
- "@types/trusted-types": "^2.0.2"
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
}
},
- "node_modules/load-ip-set": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/load-ip-set/-/load-ip-set-3.0.1.tgz",
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lilconfig": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
+ "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antonk52"
+ }
+ },
+ "node_modules/limiter": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz",
+ "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA=="
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "license": "MIT"
+ },
+ "node_modules/lit": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.0.tgz",
+ "integrity": "sha512-DGVsqsOIHBww2DqnuZzW7QsuCdahp50ojuDaBPC7jUDRpYoH0z7kHBBYZewRzer75FwtrkmkKk7iOAwSaWdBmw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@lit/reactive-element": "^2.1.0",
+ "lit-element": "^4.2.0",
+ "lit-html": "^3.3.0"
+ }
+ },
+ "node_modules/lit-element": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.2.tgz",
+ "integrity": "sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@lit-labs/ssr-dom-shim": "^1.5.0",
+ "@lit/reactive-element": "^2.1.0",
+ "lit-html": "^3.3.0"
+ }
+ },
+ "node_modules/lit-html": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.2.tgz",
+ "integrity": "sha512-Qy9hU88zcmaxBXcc10ZpdK7cOLXvXpRoBxERdtqV9QOrfpMZZ6pSYP91LhpPtap3sFMUiL7Tw2RImbe0Al2/kw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@types/trusted-types": "^2.0.2"
+ }
+ },
+ "node_modules/load-ip-set": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/load-ip-set/-/load-ip-set-3.0.1.tgz",
"integrity": "sha512-ZFZt1g4Exq01SFtKjffqau+L4Qibt+51utymHHiWo8Iu/W7LYSqE7fiZ/iAZ6dIqbmeU6ICSIK02IizSScBkLQ==",
"funding": [
{
@@ -22383,6 +22890,16 @@
"localforage": "^1.7.4"
}
},
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
"node_modules/magnet-uri": {
"version": "7.0.7",
"resolved": "https://registry.npmjs.org/magnet-uri/-/magnet-uri-7.0.7.tgz",
@@ -23109,9 +23626,9 @@
"license": "ISC"
},
"node_modules/nanoid": {
- "version": "3.3.11",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
- "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "version": "3.3.15",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz",
+ "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==",
"funding": [
{
"type": "github",
@@ -26592,6 +27109,20 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/obug": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz",
+ "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==",
+ "dev": true,
+ "funding": [
+ "https://github.com/sponsors/sxzz",
+ "https://opencollective.com/debug"
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
"node_modules/ofetch": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.5.1.tgz",
@@ -27063,6 +27594,13 @@
"node": ">=8"
}
},
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/pathval": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz",
@@ -27264,9 +27802,9 @@
}
},
"node_modules/postcss": {
- "version": "8.5.12",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz",
- "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==",
+ "version": "8.5.15",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
+ "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
"funding": [
{
"type": "opencollective",
@@ -27283,7 +27821,7 @@
],
"license": "MIT",
"dependencies": {
- "nanoid": "^3.3.11",
+ "nanoid": "^3.3.12",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@@ -29317,6 +29855,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/signal-exit": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.0.2.tgz",
@@ -29918,6 +30463,13 @@
"nan": "^2.23.0"
}
},
+ "node_modules/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/stacktrace-parser": {
"version": "0.1.11",
"resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.11.tgz",
@@ -29958,6 +30510,13 @@
"node": ">= 0.6"
}
},
+ "node_modules/std-env": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz",
+ "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/stream-shift": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz",
@@ -30776,6 +31335,13 @@
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
"license": "MIT"
},
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/tinyexec": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
@@ -30784,9 +31350,9 @@
"license": "MIT"
},
"node_modules/tinyglobby": {
- "version": "0.2.16",
- "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
- "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
@@ -30805,6 +31371,16 @@
"integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==",
"license": "ISC"
},
+ "node_modules/tinyrainbow": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
+ "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/tmp": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
@@ -31898,44 +32474,579 @@
}
}
},
- "node_modules/vm-browserify": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz",
- "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==",
- "license": "MIT"
- },
- "node_modules/wagmi": {
- "version": "2.19.5",
- "resolved": "https://registry.npmjs.org/wagmi/-/wagmi-2.19.5.tgz",
- "integrity": "sha512-RQUfKMv6U+EcSNNGiPbdkDtJwtuFxZWLmvDiQmjjBgkuPulUwDJsKhi7gjynzJdsx2yDqhHCXkKsbbfbIsHfcQ==",
+ "node_modules/vite": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.0.tgz",
+ "integrity": "sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@wagmi/connectors": "6.2.0",
- "@wagmi/core": "2.22.1",
- "use-sync-external-store": "1.4.0"
+ "lightningcss": "^1.32.0",
+ "picomatch": "^4.0.4",
+ "postcss": "^8.5.15",
+ "rolldown": "~1.1.2",
+ "tinyglobby": "^0.2.17"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
},
"funding": {
- "url": "https://github.com/sponsors/wevm"
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
},
"peerDependencies": {
- "@tanstack/react-query": ">=5.0.0",
- "react": ">=18",
- "typescript": ">=5.0.4",
- "viem": "2.x"
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "@vitejs/devtools": "^0.3.0",
+ "esbuild": "^0.27.0 || ^0.28.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
},
"peerDependenciesMeta": {
- "typescript": {
+ "@types/node": {
"optional": true
- }
- }
- },
- "node_modules/wagmi/node_modules/use-sync-external-store": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz",
- "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==",
- "license": "MIT",
- "peerDependencies": {
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "@vitejs/devtools": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite/node_modules/@emnapi/core": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
+ "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.2",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/vite/node_modules/@emnapi/runtime": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
+ "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/vite/node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
+ "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/vite/node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz",
+ "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@tybys/wasm-util": "^0.10.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "peerDependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1"
+ }
+ },
+ "node_modules/vite/node_modules/@oxc-project/types": {
+ "version": "0.137.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.137.0.tgz",
+ "integrity": "sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ }
+ },
+ "node_modules/vite/node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.2.tgz",
+ "integrity": "sha512-2cZ+7xRS+DBcuJBJKnfzsbleumJhBqSlJVpuzHC0nTqfd3QQ7Vx2/x5YR/D7cBamKSeWplwo82Fn9lqYUDEMfA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/vite/node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.2.tgz",
+ "integrity": "sha512-RkPMJnygxsgOYdkfqgpwY0/Fzm8d0VQe6HGU2/B00Xa9eqdLbrII+DOKAodbJAn3ZL1AJxGHkZRPYazgGY6Ljw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/vite/node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.2.tgz",
+ "integrity": "sha512-Uiczh6vFhwyfd7WNe7Q7mCA4KxAiLdz7jPE/WGizfRpIieoyFuNVMmM8HqZ9HwudTkY6/AeMQwlNJ9NJijguWw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/vite/node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.2.tgz",
+ "integrity": "sha512-+TpdtTRgHiJFjCVFbw311SuLk3KfytPOQQn+VlAEv+gBxYPtL7E6JS9e/tk+8CwxhIZvemJKo4rTKgfWNsKkkA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/vite/node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.2.tgz",
+ "integrity": "sha512-4lv1/tkmi7ueIVHnyreaOeUpiZP26BH9rRy6hoYfR9310A2B9nUEVRDvBx69vx64Nr3eTPPRkyciqJJs+j9Jmw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/vite/node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.2.tgz",
+ "integrity": "sha512-gBSUVO0eaWgw1JMjK3gB8BMlX2Mk148s2lTiVT3e9vjVxbl7UDfMWWY8CfIaaqiXuM9fVTMxIpUz6CAo/B6Vlw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/vite/node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.2.tgz",
+ "integrity": "sha512-LjQP/iZLBu8o8PjIfk4x3At0/mT6h282pvz8Z5LAyhGbu/kDezyO7ea62rF5uoqmgnIYqbN/MqJ3Si3Aymi7xQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/vite/node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.2.tgz",
+ "integrity": "sha512-JY4w85pU3iAiJVMh5nuk4/Mh9GjMsupe8MrIN53rwxAZW64GKrWeJBuN6SxQg9QTU5uB1cxyhDzW8jqRn1EABw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/vite/node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.2.tgz",
+ "integrity": "sha512-xvpA7o5KCYLB0Rwscmuylb1/zHHSUx4g4xilm4prC5jP76pEUlzBmMbgpbh7bVDbId4NcfT96gN5i6mE6UDaiw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/vite/node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.2.tgz",
+ "integrity": "sha512-p/ts6KBLjuk49Bp21XH77poQGt02iNz7ChgHep7tudPOaLinR/De/RHdxF8w8Yj4r/bF/bqXwH6PZrB2sA+Nvw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/vite/node_modules/@rolldown/binding-wasm32-wasi": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.2.tgz",
+ "integrity": "sha512-VMu/wmrZ9hJzYlRhbw7jK5PODlugyKZ5mOdX78+lS8OvuFkWNQdz1pFLrI2p3P0pjXOmUZ7B48o5VnMH9QOGtg==",
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "1.11.1",
+ "@emnapi/runtime": "1.11.1",
+ "@napi-rs/wasm-runtime": "^1.1.5"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/vite/node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.2.tgz",
+ "integrity": "sha512-xtUJqs8qEkuSviS0n1tsohaPuz3a1SPhZywOji4Oo+sgrJs8daEDMZ0QtqL0OS7dx8PoVpg2J/ZZycPY5I2+Zg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/vite/node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.2.tgz",
+ "integrity": "sha512-85YiLQqjUKgSO/Zjnf9e0XIn5Ymrh1fLDWBeAkZqpuBR/3R8TpfoHXuyblqyQrftSSgWO9qpcHN8mkyKsLraoA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/vite/node_modules/@rolldown/pluginutils": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
+ "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vite/node_modules/@tybys/wasm-util": {
+ "version": "0.10.3",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
+ "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/vite/node_modules/rolldown": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.2.tgz",
+ "integrity": "sha512-x0CrQQqCXWGeI8dTvFfN/Dnv3yMKT9hv5jFjlOreKAx9wqLq9wz7VvLLHyaAXC90/CpggTu9SisSbsJJTPSjNQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "=0.137.0",
+ "@rolldown/pluginutils": "^1.0.0"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm64": "1.1.2",
+ "@rolldown/binding-darwin-arm64": "1.1.2",
+ "@rolldown/binding-darwin-x64": "1.1.2",
+ "@rolldown/binding-freebsd-x64": "1.1.2",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.1.2",
+ "@rolldown/binding-linux-arm64-gnu": "1.1.2",
+ "@rolldown/binding-linux-arm64-musl": "1.1.2",
+ "@rolldown/binding-linux-ppc64-gnu": "1.1.2",
+ "@rolldown/binding-linux-s390x-gnu": "1.1.2",
+ "@rolldown/binding-linux-x64-gnu": "1.1.2",
+ "@rolldown/binding-linux-x64-musl": "1.1.2",
+ "@rolldown/binding-openharmony-arm64": "1.1.2",
+ "@rolldown/binding-wasm32-wasi": "1.1.2",
+ "@rolldown/binding-win32-arm64-msvc": "1.1.2",
+ "@rolldown/binding-win32-x64-msvc": "1.1.2"
+ }
+ },
+ "node_modules/vitest": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz",
+ "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/expect": "4.1.9",
+ "@vitest/mocker": "4.1.9",
+ "@vitest/pretty-format": "4.1.9",
+ "@vitest/runner": "4.1.9",
+ "@vitest/snapshot": "4.1.9",
+ "@vitest/spy": "4.1.9",
+ "@vitest/utils": "4.1.9",
+ "es-module-lexer": "^2.0.0",
+ "expect-type": "^1.3.0",
+ "magic-string": "^0.30.21",
+ "obug": "^2.1.1",
+ "pathe": "^2.0.3",
+ "picomatch": "^4.0.3",
+ "std-env": "^4.0.0-rc.1",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^1.0.2",
+ "tinyglobby": "^0.2.15",
+ "tinyrainbow": "^3.1.0",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@opentelemetry/api": "^1.9.0",
+ "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
+ "@vitest/browser-playwright": "4.1.9",
+ "@vitest/browser-preview": "4.1.9",
+ "@vitest/browser-webdriverio": "4.1.9",
+ "@vitest/coverage-istanbul": "4.1.9",
+ "@vitest/coverage-v8": "4.1.9",
+ "@vitest/ui": "4.1.9",
+ "happy-dom": "*",
+ "jsdom": "*",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser-playwright": {
+ "optional": true
+ },
+ "@vitest/browser-preview": {
+ "optional": true
+ },
+ "@vitest/browser-webdriverio": {
+ "optional": true
+ },
+ "@vitest/coverage-istanbul": {
+ "optional": true
+ },
+ "@vitest/coverage-v8": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ },
+ "vite": {
+ "optional": false
+ }
+ }
+ },
+ "node_modules/vitest/node_modules/es-module-lexer": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz",
+ "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vitest/node_modules/tinyexec": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz",
+ "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vm-browserify": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz",
+ "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==",
+ "license": "MIT"
+ },
+ "node_modules/wagmi": {
+ "version": "2.19.5",
+ "resolved": "https://registry.npmjs.org/wagmi/-/wagmi-2.19.5.tgz",
+ "integrity": "sha512-RQUfKMv6U+EcSNNGiPbdkDtJwtuFxZWLmvDiQmjjBgkuPulUwDJsKhi7gjynzJdsx2yDqhHCXkKsbbfbIsHfcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@wagmi/connectors": "6.2.0",
+ "@wagmi/core": "2.22.1",
+ "use-sync-external-store": "1.4.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/wevm"
+ },
+ "peerDependencies": {
+ "@tanstack/react-query": ">=5.0.0",
+ "react": ">=18",
+ "typescript": ">=5.0.4",
+ "viem": "2.x"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/wagmi/node_modules/use-sync-external-store": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz",
+ "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/web-encoding": {
@@ -32287,6 +33398,23 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/why-is-node-running": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/widest-line": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz",