Eksplorator walidatorów IOTA inspirowany gmonads.com.
Strona główna to interaktywny globus 3D z grupami walidatorów rozmieszczonymi
geograficznie, plus dashboard z metrykami sieci i ostatnimi transakcjami.
Pełna lista walidatorów, podstrony szczegółów, wykresy i kalkulator stakingu
żyją pod osobnymi route'ami (/validators, /charts, /staking).
- Next.js 16 + React 19 (App Router, RSC tam gdzie ma sens, klient tam gdzie potrzeba interakcji)
- Tailwind CSS 4 + shadcn/ui (Base UI) — system designu i komponenty
- react-globe.gl (Three.js) — globus 3D
- SWR — polling i cache po stronie klienta
- Zustand — stan UI (wybór sieci, preferencje wizualne)
- Recharts — wykresy
- Bun — runner i package manager
Komitet walidatorów IOTA jest zafiksowany na epokę (~24h). W trakcie epoki realnie zmieniają się APY, gas price, accumulated rewards i metadane. Subskrypcje WS w IOTA RPC są oznaczone jako deprecated, więc używam SWR z różnymi interwałami w zależności od dynamiki danych:
| Dane | Interwał | Powód |
|---|---|---|
| Lista walidatorów + geo | 5 min | Set walidatorów stabilny w epoce |
| Metryki sieci (TPS, gas, supply) | 30 s | Zmieniają się ciągle |
| Recent transactions | 6 s | Tylko gdy sekcja widoczna (Page Visibility / pause) |
| Ostatnie epoki | 5 min | Zmiana raz na ~24h |
| RPC status | 15 s | Health-check |
Polling pauzuje się, kiedy zakładka jest niewidoczna albo sekcja transakcji jest schowana — żeby nie palić quoty RPC.
Klient nigdy nie woła publicznego RPC bezpośrednio. Wszystkie zapytania
przechodzą przez własne route handlery (/api/validators, /api/network,
/api/transactions, /api/epoch(s), /api/rpc-status):
- URL i klucz RPC są server-only —
import 'server-only'w lib/iota/rpc-client.ts, env-vars bez prefiksuNEXT_PUBLIC_. Bundle klienta nie zawiera adresu upstreamu. - Rate limit per-IP (token bucket, 60 req/min, burst 10) zaimplementowany
in-memory w lib/rate-limit.ts. Klucz biorę z zaufanych
nagłówków platformy (
cf-connecting-ip,x-vercel-forwarded-for, …);x-forwarded-forjest opt-in, żeby nie spoofować klucza w lokalnym devie. Na produkcji to soft limit per-instancja — pod docelowy ruch wymieniłbym na Upstash/Vercel KV. - Cache-Control na każdym route (np.
s-maxage=60, stale-while-revalidate=300) żeby CDN absorbował duplikaty. - In-process cache + dedupe inflight requestów w lib/iota/validators.ts — równoległe wywołania dzielą jeden roundtrip do RPC.
- Timeout 10 s + AbortController na każdym wywołaniu RPC.
Domyślnie publiczny endpoint api.mainnet.iota.cafe. Wartości można nadpisać
zmiennymi środowiskowymi (poniżej). Sieć przełączana w UI jest trzymana w URL
(?network=...) i w zustand store, żeby było shareable.
- klik na walidatora w tabeli →
/validators/[address]z pełnymi danymi i mini-mapą, - sortowalna tabela (voting power, stake, APY, commission), paginacja, badge dla niezgeolocalizowanych,
- na globusie dwa poziomy LOD: kontynenty z daleka, kraje po przybliżeniu (zoom band hysteresis, żeby nie migotało),
- „komety" między walidatorami na globe można wyłączyć w nagłówku (mobile = mniej, dla wydajności),
/stakingto czysty kalkulator APY na bazie publicznych danych pool — bez wallet connect (to znacząco większy zakres, nie był wymagany).
IOTA RPC zwraca netAddress typu /dns/host/tcp/... lub /ip4/.../tcp/...,
nie współrzędne. Pipeline w lib/iota/geo.ts:
- Parsowanie hosta z multiaddr (lib/iota/net-address.ts)
dns.resolve4()→ IP- Batch POST do
ip-api.com(do 100 IP / request) - In-memory cache na 24h, kluczowane hostem
Walidatory bez geo lecą do tabeli z badge „Unknown location" — nie pomijam ich,
bo to wciąż walidne członki komitetu. Free tier ip-api.com jest tylko
HTTP, więc plain-text request jest opt-in (IP_API_ALLOW_INSECURE_HTTP=true)
i jest świadomym kompromisem dev/recruitment — pod prod zamieniłbym na płatny
ip-api pro / ipinfo / własną tablicę GeoIP.
react-globe.gl jest ładowany przez next/dynamic z ssr: false. Po
zamontowaniu sprawdzam dostępność WebGL — jeśli kontekstu nie da się pobrać,
zamiast Three.js renderuję statyczny obraz globusa z komunikatem (dane
walidatorów dalej działają w tabeli niżej). Liczba neonowych komet jest
zmniejszona dla viewportów < 640px.
Specyfikacja mówiła o danych z explorer.iota.org/validators, ale ta strona
to też tylko klient renderujący dane z tego samego JSON-RPC. Zamiast scrapingu
HTML czytam więc bezpośrednio RPC (iotax_getLatestIotaSystemStateV2,
iotax_getValidatorsApy, iotax_queryTransactionBlocks, …) — stabilniej,
szybciej, łatwiej do cache'a.
bun install
bun devAplikacja na http://localhost:3000.
bun run lint # ESLint
bun run test # node --test (tests/*.test.mts)
bun run build # production build# RPC — wszystkie server-only, brak NEXT_PUBLIC_
IOTA_RPC_URL=https://api.mainnet.iota.cafe # alias dla mainnet
IOTA_MAINNET_RPC_URL=... # opcjonalnie nadpisuje powyższe
IOTA_TESTNET_RPC_URL=https://api.testnet.iota.cafe
IOTA_RPC_KEY=... # wspólny klucz, jeśli provider go wymaga
IOTA_MAINNET_RPC_KEY=...
IOTA_TESTNET_RPC_KEY=...
# Geo enrichment (opcjonalne — bez tego walidatory dostaną „Unknown location")
IP_API_URL=http://ip-api.com/batch?fields=status,message,query,lat,lon,continent,country,city
IP_API_ALLOW_INSECURE_HTTP=true # świadoma zgoda na HTTP do free tier ip-api
# Rate limit
TRUST_X_FORWARDED_FOR=true # tylko jeśli zaufany proxyapp/
api/ # route handlery, server-only proxy do IOTA RPC
validators/ # /validators, /validators/[address]
transactions/[digest] # detal transakcji
charts/, staking/
components/
globe/ # Three.js + grouping, LOD, label rendering
validators/, home/, staking/, charts/, ui/
lib/
iota/ # RPC client, geo, types, network/validator/tx queries
rate-limit.ts, api-utils.ts, network-store.ts, …
tests/ # jednostkowe (node --test): formattery, parser multiaddr, RPC config