solanapolis 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/README.md +518 -0
  2. package/bin/solanapolis.js +197 -0
  3. package/convex/_generated/api.d.ts +175 -0
  4. package/convex/_generated/api.js +23 -0
  5. package/convex/_generated/dataModel.d.ts +60 -0
  6. package/convex/_generated/server.d.ts +143 -0
  7. package/convex/_generated/server.js +93 -0
  8. package/convex/agent/conversation.ts +352 -0
  9. package/convex/agent/embeddingsCache.ts +110 -0
  10. package/convex/agent/memory.ts +450 -0
  11. package/convex/agent/schema.ts +53 -0
  12. package/convex/aiChat.ts +54 -0
  13. package/convex/aiTown/agent.ts +382 -0
  14. package/convex/aiTown/agentDescription.ts +27 -0
  15. package/convex/aiTown/agentInputs.ts +155 -0
  16. package/convex/aiTown/agentOperations.ts +178 -0
  17. package/convex/aiTown/conversation.ts +395 -0
  18. package/convex/aiTown/conversationMembership.ts +38 -0
  19. package/convex/aiTown/game.ts +371 -0
  20. package/convex/aiTown/ids.ts +32 -0
  21. package/convex/aiTown/inputHandler.ts +9 -0
  22. package/convex/aiTown/inputs.ts +25 -0
  23. package/convex/aiTown/insertInput.ts +20 -0
  24. package/convex/aiTown/location.ts +32 -0
  25. package/convex/aiTown/main.ts +154 -0
  26. package/convex/aiTown/movement.ts +189 -0
  27. package/convex/aiTown/player.ts +310 -0
  28. package/convex/aiTown/playerDescription.ts +35 -0
  29. package/convex/aiTown/schema.ts +79 -0
  30. package/convex/aiTown/world.ts +65 -0
  31. package/convex/aiTown/worldMap.ts +74 -0
  32. package/convex/chat.ts +79 -0
  33. package/convex/constants.ts +78 -0
  34. package/convex/convex.config.ts +6 -0
  35. package/convex/crons.ts +89 -0
  36. package/convex/engine/abstractGame.ts +199 -0
  37. package/convex/engine/historicalObject.ts +355 -0
  38. package/convex/engine/schema.ts +56 -0
  39. package/convex/http.ts +36 -0
  40. package/convex/init.ts +110 -0
  41. package/convex/messages.ts +53 -0
  42. package/convex/npcCarAgents.ts +415 -0
  43. package/convex/schema.ts +61 -0
  44. package/convex/streaming.ts +23 -0
  45. package/convex/testing.ts +202 -0
  46. package/convex/tsconfig.json +18 -0
  47. package/convex/util/FastIntegerCompression.ts +221 -0
  48. package/convex/util/assertNever.ts +4 -0
  49. package/convex/util/asyncMap.ts +20 -0
  50. package/convex/util/compression.ts +71 -0
  51. package/convex/util/geometry.ts +132 -0
  52. package/convex/util/isSimpleObject.ts +11 -0
  53. package/convex/util/llm.ts +724 -0
  54. package/convex/util/minheap.ts +38 -0
  55. package/convex/util/object.ts +22 -0
  56. package/convex/util/sleep.ts +3 -0
  57. package/convex/util/types.ts +33 -0
  58. package/convex/util/xxhash.ts +228 -0
  59. package/convex/world.ts +257 -0
  60. package/data/animations/campfire.json +45 -0
  61. package/data/animations/gentlesparkle.json +37 -0
  62. package/data/animations/gentlesplash.json +61 -0
  63. package/data/animations/gentlewaterfall.json +61 -0
  64. package/data/animations/windmill.json +78 -0
  65. package/data/characters.ts +121 -0
  66. package/data/convertMap.js +74 -0
  67. package/data/gentle.js +330 -0
  68. package/data/spritesheets/f1.ts +75 -0
  69. package/data/spritesheets/f2.ts +75 -0
  70. package/data/spritesheets/f3.ts +75 -0
  71. package/data/spritesheets/f4.ts +75 -0
  72. package/data/spritesheets/f5.ts +75 -0
  73. package/data/spritesheets/f6.ts +75 -0
  74. package/data/spritesheets/f7.ts +75 -0
  75. package/data/spritesheets/f8.ts +75 -0
  76. package/data/spritesheets/p1.ts +59 -0
  77. package/data/spritesheets/p2.ts +59 -0
  78. package/data/spritesheets/p3.ts +59 -0
  79. package/data/spritesheets/player.ts +59 -0
  80. package/data/spritesheets/types.ts +26 -0
  81. package/eslint.config.mjs +37 -0
  82. package/next.config.ts +7 -0
  83. package/package.json +85 -0
  84. package/postcss.config.mjs +7 -0
  85. package/public/file.svg +1 -0
  86. package/public/globe.svg +1 -0
  87. package/public/helius-icon.svg +84 -0
  88. package/public/helius-logo.svg +85 -0
  89. package/public/next.svg +1 -0
  90. package/public/plane.glb +0 -0
  91. package/public/vercel.svg +1 -0
  92. package/public/window.svg +1 -0
  93. package/scripts/clear-city.ts +74 -0
  94. package/scripts/seed-wallets.ts +185 -0
  95. package/scripts/setup-webhook.ts +73 -0
  96. package/src/app/api/auth/callback/route.ts +6 -0
  97. package/src/app/api/auth/link-wallet/route.ts +6 -0
  98. package/src/app/api/auth/phantom/route.ts +6 -0
  99. package/src/app/api/broadcast-position/route.ts +59 -0
  100. package/src/app/api/leaderboard/route.ts +85 -0
  101. package/src/app/api/network-stats/route.ts +86 -0
  102. package/src/app/api/parcel-reward/route.ts +181 -0
  103. package/src/app/api/queue-status/route.ts +30 -0
  104. package/src/app/api/snapshots/route.ts +37 -0
  105. package/src/app/api/transactions/enhanced/route.ts +57 -0
  106. package/src/app/api/treasury/route.ts +83 -0
  107. package/src/app/api/wallet/[address]/balances/route.ts +124 -0
  108. package/src/app/api/wallet/[address]/identity/route.ts +32 -0
  109. package/src/app/api/wallet/[address]/route.ts +216 -0
  110. package/src/app/api/wallet/[address]/traded-tokens/route.ts +41 -0
  111. package/src/app/api/wallets/route.ts +68 -0
  112. package/src/app/api/webhooks/helius/route.ts +76 -0
  113. package/src/app/auth/callback/page.tsx +29 -0
  114. package/src/app/favicon.ico +0 -0
  115. package/src/app/globals.css +39 -0
  116. package/src/app/layout.tsx +43 -0
  117. package/src/app/page.tsx +16 -0
  118. package/src/components/AITownNPCs.tsx +206 -0
  119. package/src/components/ActivityFeed.tsx +189 -0
  120. package/src/components/AuthPanel.tsx +163 -0
  121. package/src/components/BeachScene.tsx +280 -0
  122. package/src/components/Building.tsx +138 -0
  123. package/src/components/CesiumFlight.tsx +1768 -0
  124. package/src/components/CesiumGlobe.tsx +616 -0
  125. package/src/components/CitizenCard.tsx +442 -0
  126. package/src/components/CitizenCardModal.tsx +153 -0
  127. package/src/components/CityGrid.tsx +313 -0
  128. package/src/components/CityLandmarks.tsx +427 -0
  129. package/src/components/CityScene.tsx +1289 -0
  130. package/src/components/CitySlotsBadge.tsx +68 -0
  131. package/src/components/CockpitHUD.tsx +460 -0
  132. package/src/components/ConvexWrapper.tsx +19 -0
  133. package/src/components/DubaiDistrict.tsx +630 -0
  134. package/src/components/FlightMiniMap.tsx +133 -0
  135. package/src/components/GameChat.tsx +383 -0
  136. package/src/components/GameHUD.tsx +393 -0
  137. package/src/components/Ground.tsx +14 -0
  138. package/src/components/HowItWorksModal.tsx +251 -0
  139. package/src/components/IngestionBanner.tsx +123 -0
  140. package/src/components/InstancedBuildings.tsx +316 -0
  141. package/src/components/InstancedCars.tsx +504 -0
  142. package/src/components/InstancedCityPlanes.tsx +259 -0
  143. package/src/components/InstancedHouses.tsx +246 -0
  144. package/src/components/InstancedLampPosts.tsx +201 -0
  145. package/src/components/InstancedResidentCars.tsx +357 -0
  146. package/src/components/InstancedRoadDashes.tsx +42 -0
  147. package/src/components/InstancedSkyscrapers.tsx +434 -0
  148. package/src/components/InstancedTrees.tsx +67 -0
  149. package/src/components/LeaderboardPanel.tsx +136 -0
  150. package/src/components/MultiplayerPlanes.tsx +128 -0
  151. package/src/components/NetworkStats.tsx +83 -0
  152. package/src/components/NewBuildingSpotlight.tsx +93 -0
  153. package/src/components/ParcelChallengeBanner.tsx +242 -0
  154. package/src/components/ParcelReward.tsx +191 -0
  155. package/src/components/Park.tsx +42 -0
  156. package/src/components/PhantomWrapper.tsx +22 -0
  157. package/src/components/PixelStreamViewer.tsx +335 -0
  158. package/src/components/PlaneMode.tsx +190 -0
  159. package/src/components/PlayerCar.tsx +211 -0
  160. package/src/components/PlayerPlane.tsx +255 -0
  161. package/src/components/ProjectileRenderer.tsx +249 -0
  162. package/src/components/QueueStatusBanner.tsx +86 -0
  163. package/src/components/RealPlayerTags.tsx +82 -0
  164. package/src/components/SceneLighting.tsx +382 -0
  165. package/src/components/SelectionBeam.tsx +59 -0
  166. package/src/components/SwapPanel.tsx +104 -0
  167. package/src/components/SwapParticles.tsx +237 -0
  168. package/src/components/TreasureGate.tsx +505 -0
  169. package/src/components/WalletPanel.tsx +421 -0
  170. package/src/components/WalletSearch.tsx +244 -0
  171. package/src/components/WelcomeOverlay.tsx +135 -0
  172. package/src/components/WindowTooltip.tsx +498 -0
  173. package/src/context/AuthContext.tsx +230 -0
  174. package/src/lib/bot-detection.ts +125 -0
  175. package/src/lib/building-math.ts +136 -0
  176. package/src/lib/building-shader.ts +253 -0
  177. package/src/lib/car-paths.ts +244 -0
  178. package/src/lib/car-system.ts +182 -0
  179. package/src/lib/city-constants.ts +29 -0
  180. package/src/lib/city-slots.ts +35 -0
  181. package/src/lib/city-zoning.ts +64 -0
  182. package/src/lib/collision-map.ts +147 -0
  183. package/src/lib/day-night.ts +252 -0
  184. package/src/lib/export-card.ts +28 -0
  185. package/src/lib/helius-webhook.ts +90 -0
  186. package/src/lib/helius.ts +74 -0
  187. package/src/lib/house-shader.ts +119 -0
  188. package/src/lib/mock-data.ts +56 -0
  189. package/src/lib/multiplayer-manager.ts +329 -0
  190. package/src/lib/plane-physics.ts +66 -0
  191. package/src/lib/player-car.ts +147 -0
  192. package/src/lib/player-plane.ts +200 -0
  193. package/src/lib/projectile-system.ts +272 -0
  194. package/src/lib/skyscraper-types.ts +52 -0
  195. package/src/lib/sound-engine.ts +464 -0
  196. package/src/lib/supabase-admin.ts +9 -0
  197. package/src/lib/supabase.ts +8 -0
  198. package/src/lib/swap-events.ts +70 -0
  199. package/src/middleware.ts +37 -0
  200. package/src/types/phantom.d.ts +16 -0
  201. package/src/types/wallet.ts +20 -0
  202. package/tsconfig.json +34 -0
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Creates the Helius swap webhook and seeds it with all existing wallet addresses.
3
+ *
4
+ * Usage:
5
+ * source .env.local && npx tsx scripts/setup-webhook.ts https://heliopolis.ngrok.app
6
+ *
7
+ * Outputs the env vars to add to .env.local.
8
+ */
9
+ import { createClient } from "@supabase/supabase-js";
10
+
11
+ const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL!;
12
+ const SUPABASE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY!;
13
+ const HELIUS_API_KEY = process.env.HELIUS_API_KEY!;
14
+
15
+ const appUrl = process.argv[2];
16
+ if (!appUrl) {
17
+ console.error("Usage: npx tsx scripts/setup-webhook.ts <app-url>");
18
+ console.error("Example: npx tsx scripts/setup-webhook.ts https://heliopolis.ngrok.app");
19
+ process.exit(1);
20
+ }
21
+
22
+ const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
23
+
24
+ async function main() {
25
+ // Get all wallet addresses
26
+ const { data: wallets, error } = await supabase
27
+ .from("wallets")
28
+ .select("address")
29
+ .limit(100000);
30
+
31
+ if (error) { console.error(error); process.exit(1); }
32
+
33
+ const addresses = wallets.map((w: { address: string }) => w.address);
34
+ console.log(`Found ${addresses.length} wallets to track`);
35
+
36
+ // Generate secret
37
+ const secret = crypto.randomUUID();
38
+ const webhookUrl = `${appUrl.replace(/\/$/, "")}/api/webhooks/helius`;
39
+
40
+ console.log(`Creating webhook → ${webhookUrl}`);
41
+
42
+ const res = await fetch(
43
+ `https://api.helius.xyz/v0/webhooks?api-key=${HELIUS_API_KEY}`,
44
+ {
45
+ method: "POST",
46
+ headers: { "Content-Type": "application/json" },
47
+ body: JSON.stringify({
48
+ webhookURL: webhookUrl,
49
+ transactionTypes: ["SWAP"],
50
+ accountAddresses: addresses,
51
+ webhookType: "enhanced",
52
+ authHeader: `Bearer ${secret}`,
53
+ }),
54
+ },
55
+ );
56
+
57
+ if (!res.ok) {
58
+ const body = await res.text();
59
+ console.error(`Failed to create webhook: ${res.status} ${body}`);
60
+ process.exit(1);
61
+ }
62
+
63
+ const data = await res.json();
64
+ const webhookId = data.webhookID;
65
+
66
+ console.log("\nWebhook created successfully!\n");
67
+ console.log("Add these to your .env.local:\n");
68
+ console.log(`HELIUS_WEBHOOK_ID=${webhookId}`);
69
+ console.log(`HELIUS_WEBHOOK_SECRET=${secret}`);
70
+ console.log(`NEXT_PUBLIC_MOCK_SWAPS=false`);
71
+ }
72
+
73
+ main();
@@ -0,0 +1,6 @@
1
+ import { NextResponse } from "next/server";
2
+
3
+ // Auth disabled — city is read-only
4
+ export async function GET(request: Request) {
5
+ return NextResponse.redirect(new URL(request.url).origin);
6
+ }
@@ -0,0 +1,6 @@
1
+ import { NextResponse } from "next/server";
2
+
3
+ // Auth disabled — city is read-only
4
+ export async function POST() {
5
+ return NextResponse.json({ error: "Auth disabled" }, { status: 403 });
6
+ }
@@ -0,0 +1,6 @@
1
+ import { NextResponse } from "next/server";
2
+
3
+ // Auth disabled — city is read-only
4
+ export async function POST() {
5
+ return NextResponse.json({ error: "Auth disabled" }, { status: 403 });
6
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * POST /api/broadcast-position
3
+ *
4
+ * Relays UE5 Pixel Streaming client positions into the Supabase Realtime
5
+ * channel that CesiumFlight.tsx listens to. This lets UE5 pilots appear
6
+ * as remote-plane entities alongside web pilots.
7
+ *
8
+ * Body: { wallet, lon, lat, alt, heading, pitch, roll, source? }
9
+ */
10
+
11
+ import { NextRequest, NextResponse } from "next/server";
12
+ import { createClient } from "@supabase/supabase-js";
13
+
14
+ const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
15
+ const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
16
+
17
+ export async function POST(req: NextRequest) {
18
+ let body: {
19
+ wallet: string;
20
+ lon: number;
21
+ lat: number;
22
+ alt: number;
23
+ heading: number;
24
+ pitch: number;
25
+ roll: number;
26
+ source?: string;
27
+ };
28
+
29
+ try {
30
+ body = await req.json();
31
+ } catch {
32
+ return NextResponse.json({ error: "Invalid JSON" }, { status: 400 });
33
+ }
34
+
35
+ const { wallet, lon, lat, alt, heading, pitch, roll, source = "web" } = body;
36
+
37
+ if (!wallet || lon === undefined || lat === undefined) {
38
+ return NextResponse.json({ error: "Missing required fields" }, { status: 400 });
39
+ }
40
+
41
+ // Use service role so we can broadcast without an authenticated session
42
+ const supabase = createClient(supabaseUrl, serviceKey, {
43
+ realtime: { params: { eventsPerSecond: 20 } },
44
+ });
45
+
46
+ const channel = supabase.channel("heliopolis-cesium-planes");
47
+ await channel.subscribe();
48
+
49
+ await channel.send({
50
+ type: "broadcast",
51
+ event: "cesium-position",
52
+ payload: { wallet, lon, lat, alt, heading, pitch, roll, source },
53
+ });
54
+
55
+ // Clean up immediately — this is a fire-and-forget relay
56
+ await supabase.removeChannel(channel);
57
+
58
+ return NextResponse.json({ ok: true });
59
+ }
@@ -0,0 +1,85 @@
1
+ import { NextResponse } from "next/server";
2
+ import { createAdminClient } from "@/lib/supabase-admin";
3
+
4
+ /**
5
+ * GET /api/leaderboard — returns top players
6
+ * POST /api/leaderboard — submits/ends a game session
7
+ */
8
+
9
+ export async function GET(request: Request) {
10
+ const { searchParams } = new URL(request.url);
11
+ const limit = Math.min(parseInt(searchParams.get("limit") ?? "20", 10), 100);
12
+
13
+ const supabase = createAdminClient();
14
+
15
+ // Get leaderboard
16
+ const { data: leaderboard, error: lbErr } = await supabase
17
+ .rpc("get_leaderboard", { n: limit });
18
+
19
+ if (lbErr) {
20
+ return NextResponse.json({ error: lbErr.message }, { status: 500 });
21
+ }
22
+
23
+ // Get online player count
24
+ const { count: onlineCount } = await supabase
25
+ .from("active_players")
26
+ .select("*", { count: "exact", head: true })
27
+ .gte("last_heartbeat", new Date(Date.now() - 30_000).toISOString());
28
+
29
+ // Cleanup stale players (opportunistic)
30
+ try { await supabase.rpc("cleanup_stale_players"); } catch { /* ignore */ }
31
+
32
+ return NextResponse.json({
33
+ leaderboard: leaderboard ?? [],
34
+ onlineCount: onlineCount ?? 0,
35
+ });
36
+ }
37
+
38
+ export async function POST(request: Request) {
39
+ try {
40
+ const body = await request.json();
41
+ const { action, wallet, mode, sessionId, score, hitsPlanes, hitsCars, shotsFired } = body;
42
+
43
+ const supabase = createAdminClient();
44
+
45
+ if (action === "start") {
46
+ // Create a new game session
47
+ const { data, error } = await supabase
48
+ .from("game_sessions")
49
+ .insert({
50
+ wallet_address: wallet,
51
+ mode: mode ?? "car",
52
+ })
53
+ .select("id")
54
+ .single();
55
+
56
+ if (error) {
57
+ return NextResponse.json({ error: error.message }, { status: 500 });
58
+ }
59
+
60
+ return NextResponse.json({ sessionId: data.id });
61
+ }
62
+
63
+ if (action === "end") {
64
+ // End a game session and update leaderboard
65
+ const { error } = await supabase.rpc("end_game_session", {
66
+ p_session_id: sessionId,
67
+ p_wallet: wallet,
68
+ p_score: score ?? 0,
69
+ p_hits_planes: hitsPlanes ?? 0,
70
+ p_hits_cars: hitsCars ?? 0,
71
+ p_shots_fired: shotsFired ?? 0,
72
+ });
73
+
74
+ if (error) {
75
+ return NextResponse.json({ error: error.message }, { status: 500 });
76
+ }
77
+
78
+ return NextResponse.json({ ok: true });
79
+ }
80
+
81
+ return NextResponse.json({ error: "Unknown action" }, { status: 400 });
82
+ } catch {
83
+ return NextResponse.json({ error: "Invalid request" }, { status: 400 });
84
+ }
85
+ }
@@ -0,0 +1,86 @@
1
+ import { NextResponse } from "next/server";
2
+
3
+ /**
4
+ * Network stats endpoint — fetches live Solana network data via Helius RPC.
5
+ * Uses standard Solana RPC methods through the Helius RPC URL.
6
+ */
7
+ export async function GET() {
8
+ const rpcUrl = process.env.HELIUS_RPC_URL;
9
+ if (!rpcUrl) {
10
+ return NextResponse.json({ error: "RPC not configured" }, { status: 500 });
11
+ }
12
+
13
+ try {
14
+ // Fetch slot, epoch, and TPS in parallel
15
+ const [slotRes, epochRes, perfRes] = await Promise.all([
16
+ fetch(rpcUrl, {
17
+ method: "POST",
18
+ headers: { "Content-Type": "application/json" },
19
+ body: JSON.stringify({
20
+ jsonrpc: "2.0",
21
+ id: "slot",
22
+ method: "getSlot",
23
+ params: [{ commitment: "confirmed" }],
24
+ }),
25
+ }),
26
+ fetch(rpcUrl, {
27
+ method: "POST",
28
+ headers: { "Content-Type": "application/json" },
29
+ body: JSON.stringify({
30
+ jsonrpc: "2.0",
31
+ id: "epoch",
32
+ method: "getEpochInfo",
33
+ params: [{ commitment: "confirmed" }],
34
+ }),
35
+ }),
36
+ fetch(rpcUrl, {
37
+ method: "POST",
38
+ headers: { "Content-Type": "application/json" },
39
+ body: JSON.stringify({
40
+ jsonrpc: "2.0",
41
+ id: "perf",
42
+ method: "getRecentPerformanceSamples",
43
+ params: [5],
44
+ }),
45
+ }),
46
+ ]);
47
+
48
+ const [slotData, epochData, perfData] = await Promise.all([
49
+ slotRes.json(),
50
+ epochRes.json(),
51
+ perfRes.json(),
52
+ ]);
53
+
54
+ // Calculate average TPS from performance samples
55
+ let avgTps = 0;
56
+ const samples = perfData.result;
57
+ if (Array.isArray(samples) && samples.length > 0) {
58
+ const totalTx = samples.reduce(
59
+ (sum: number, s: { numTransactions: number }) => sum + s.numTransactions,
60
+ 0
61
+ );
62
+ const totalSecs = samples.reduce(
63
+ (sum: number, s: { samplePeriodSecs: number }) => sum + s.samplePeriodSecs,
64
+ 0
65
+ );
66
+ avgTps = totalSecs > 0 ? Math.round(totalTx / totalSecs) : 0;
67
+ }
68
+
69
+ const epoch = epochData.result;
70
+
71
+ return NextResponse.json({
72
+ slot: slotData.result,
73
+ epoch: epoch?.epoch,
74
+ epochProgress: epoch
75
+ ? ((epoch.slotIndex / epoch.slotsInEpoch) * 100).toFixed(1)
76
+ : null,
77
+ tps: avgTps,
78
+ });
79
+ } catch (err) {
80
+ console.error("Network stats error:", err);
81
+ return NextResponse.json(
82
+ { error: String(err) },
83
+ { status: 500 }
84
+ );
85
+ }
86
+ }
@@ -0,0 +1,181 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { createClient } from "@supabase/supabase-js";
3
+
4
+ /**
5
+ * Parcel Rewards API — first 16 wallets get 0.069420 SOL hidden in their parcels.
6
+ *
7
+ * Budget: ~1.16 SOL → 16 × 0.069420 = 1.11072 SOL + tx fees
8
+ *
9
+ * GET ?wallet=<address> → check if wallet qualifies, show reward amount
10
+ * POST { wallet, action: "claim" } → claim the reward (sends SOL from treasury)
11
+ */
12
+
13
+ const REWARD_PARCEL_COUNT = 16; // 16 parcels funded
14
+ const REWARD_SOL = 0.069420; // Fixed per parcel — the magic number
15
+
16
+ function supabaseAdmin() {
17
+ return createClient(
18
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
19
+ process.env.SUPABASE_SERVICE_ROLE_KEY!,
20
+ );
21
+ }
22
+
23
+ /** Check if wallet is in the first N placed wallets */
24
+ async function getWalletRank(walletAddress: string): Promise<number | null> {
25
+ const sb = supabaseAdmin();
26
+
27
+ const { data, error } = await sb
28
+ .from("wallets")
29
+ .select("address, created_at")
30
+ .or("ingestion_status.eq.complete,ingestion_status.is.null")
31
+ .order("created_at", { ascending: true })
32
+ .limit(REWARD_PARCEL_COUNT);
33
+
34
+ if (error || !data) return null;
35
+
36
+ const idx = data.findIndex((w: { address: string }) => w.address === walletAddress);
37
+ return idx >= 0 ? idx + 1 : null;
38
+ }
39
+
40
+ /** Check if reward was already claimed */
41
+ async function isRewardClaimed(walletAddress: string): Promise<boolean> {
42
+ const sb = supabaseAdmin();
43
+ const { data } = await sb
44
+ .from("parcel_rewards")
45
+ .select("id")
46
+ .eq("wallet_address", walletAddress)
47
+ .eq("claimed", true)
48
+ .maybeSingle();
49
+ return !!data;
50
+ }
51
+
52
+ /** Record a reward claim */
53
+ async function recordClaim(walletAddress: string, amount: number, txSignature: string) {
54
+ const sb = supabaseAdmin();
55
+ await sb
56
+ .from("parcel_rewards")
57
+ .upsert({
58
+ wallet_address: walletAddress,
59
+ amount_sol: amount,
60
+ claimed: true,
61
+ tx_signature: txSignature,
62
+ claimed_at: new Date().toISOString(),
63
+ }, { onConflict: "wallet_address" });
64
+ }
65
+
66
+ // ── GET: Check reward eligibility ──────────────────────────────────────────────
67
+ export async function GET(request: NextRequest) {
68
+ const wallet = request.nextUrl.searchParams.get("wallet");
69
+ if (!wallet) {
70
+ return NextResponse.json({ error: "wallet parameter required" }, { status: 400 });
71
+ }
72
+
73
+ try {
74
+ const rank = await getWalletRank(wallet);
75
+
76
+ if (rank === null || rank > REWARD_PARCEL_COUNT) {
77
+ return NextResponse.json({
78
+ eligible: false,
79
+ message: `Only the first ${REWARD_PARCEL_COUNT} wallets get parcel rewards`,
80
+ });
81
+ }
82
+
83
+ const claimed = await isRewardClaimed(wallet);
84
+
85
+ return NextResponse.json({
86
+ eligible: true,
87
+ rank,
88
+ totalSlots: REWARD_PARCEL_COUNT,
89
+ rewardSOL: REWARD_SOL,
90
+ claimed,
91
+ message: claimed
92
+ ? "Reward already claimed!"
93
+ : `🎁 Your parcel has ${REWARD_SOL} SOL hidden inside!`,
94
+ });
95
+ } catch (err) {
96
+ console.error("Parcel reward check error:", err);
97
+ return NextResponse.json({ eligible: false, error: "Check failed" }, { status: 500 });
98
+ }
99
+ }
100
+
101
+ // ── POST: Claim reward ─────────────────────────────────────────────────────────
102
+ export async function POST(request: NextRequest) {
103
+ try {
104
+ const body = await request.json();
105
+ const { wallet, action } = body;
106
+
107
+ if (action !== "claim" || !wallet) {
108
+ return NextResponse.json({ error: "Invalid request" }, { status: 400 });
109
+ }
110
+
111
+ // Verify eligibility
112
+ const rank = await getWalletRank(wallet);
113
+ if (rank === null || rank > REWARD_PARCEL_COUNT) {
114
+ return NextResponse.json({ error: "Wallet not eligible" }, { status: 403 });
115
+ }
116
+
117
+ // Check not already claimed
118
+ const alreadyClaimed = await isRewardClaimed(wallet);
119
+ if (alreadyClaimed) {
120
+ return NextResponse.json({ error: "Reward already claimed" }, { status: 409 });
121
+ }
122
+
123
+ // Check for treasury private key
124
+ const treasuryKey = process.env.TREASURY_PRIVATE_KEY;
125
+ if (!treasuryKey) {
126
+ await recordClaim(wallet, REWARD_SOL, "pending-manual");
127
+ return NextResponse.json({
128
+ success: true,
129
+ amount: REWARD_SOL,
130
+ txSignature: "pending-manual",
131
+ message: `${REWARD_SOL} SOL reward recorded! Treasury payout pending.`,
132
+ });
133
+ }
134
+
135
+ // Send SOL from treasury wallet
136
+ const rpcUrl = process.env.HELIUS_RPC_URL || "https://api.mainnet-beta.solana.com";
137
+
138
+ const { Connection, Keypair, SystemProgram, Transaction, PublicKey, LAMPORTS_PER_SOL } = await import("@solana/web3.js");
139
+ const bs58 = (await import("bs58")).default;
140
+
141
+ const connection = new Connection(rpcUrl, "confirmed");
142
+ const treasury = Keypair.fromSecretKey(bs58.decode(treasuryKey));
143
+ const recipient = new PublicKey(wallet);
144
+ const lamports = Math.round(REWARD_SOL * LAMPORTS_PER_SOL);
145
+
146
+ // Check treasury balance
147
+ const balance = await connection.getBalance(treasury.publicKey);
148
+ if (balance < lamports + 5000) {
149
+ return NextResponse.json({
150
+ error: "Treasury depleted — all rewards have been claimed!",
151
+ treasuryBalance: balance / LAMPORTS_PER_SOL,
152
+ }, { status: 503 });
153
+ }
154
+
155
+ const transaction = new Transaction().add(
156
+ SystemProgram.transfer({
157
+ fromPubkey: treasury.publicKey,
158
+ toPubkey: recipient,
159
+ lamports,
160
+ }),
161
+ );
162
+
163
+ const txSignature = await connection.sendTransaction(transaction, [treasury]);
164
+ await connection.confirmTransaction(txSignature, "confirmed");
165
+
166
+ await recordClaim(wallet, REWARD_SOL, txSignature);
167
+
168
+ return NextResponse.json({
169
+ success: true,
170
+ amount: REWARD_SOL,
171
+ txSignature,
172
+ message: `🎉 ${REWARD_SOL} SOL sent to your wallet!`,
173
+ });
174
+ } catch (err) {
175
+ console.error("Parcel reward claim error:", err);
176
+ return NextResponse.json({
177
+ error: "Claim failed — please try again",
178
+ details: err instanceof Error ? err.message : "Unknown error",
179
+ }, { status: 500 });
180
+ }
181
+ }
@@ -0,0 +1,30 @@
1
+ import { NextResponse } from "next/server";
2
+ import { createAdminClient } from "@/lib/supabase-admin";
3
+
4
+ export async function GET() {
5
+ try {
6
+ const supabase = createAdminClient();
7
+
8
+ const { data, error } = await supabase
9
+ .from("wallets")
10
+ .select("ingestion_status");
11
+
12
+ if (error) throw new Error(error.message);
13
+
14
+ const counts = { queued: 0, processing: 0, complete: 0, failed: 0 };
15
+ for (const row of data ?? []) {
16
+ const s = row.ingestion_status as keyof typeof counts;
17
+ if (s in counts) counts[s]++;
18
+ }
19
+
20
+ return NextResponse.json(counts, {
21
+ headers: { "Cache-Control": "public, max-age=10" },
22
+ });
23
+ } catch (err) {
24
+ console.error("Queue status error:", err);
25
+ return NextResponse.json(
26
+ { error: err instanceof Error ? err.message : "Unknown error" },
27
+ { status: 500 },
28
+ );
29
+ }
30
+ }
@@ -0,0 +1,37 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { createAdminClient } from "@/lib/supabase-admin";
3
+
4
+ export async function GET(request: NextRequest) {
5
+ try {
6
+ const { searchParams } = request.nextUrl;
7
+ const rawLimit = parseInt(searchParams.get("limit") ?? "30", 10);
8
+ const limit = Math.max(1, Math.min(500, isNaN(rawLimit) ? 30 : rawLimit));
9
+
10
+ const supabase = createAdminClient();
11
+
12
+ const { data, error } = await supabase
13
+ .from("city_snapshots")
14
+ .select("*")
15
+ .order("created_at", { ascending: false })
16
+ .limit(limit);
17
+
18
+ if (error) {
19
+ throw new Error(`DB error: ${error.message}`);
20
+ }
21
+
22
+ return NextResponse.json(
23
+ { snapshots: data ?? [], count: (data ?? []).length },
24
+ {
25
+ headers: {
26
+ "Cache-Control": "public, max-age=900",
27
+ },
28
+ }
29
+ );
30
+ } catch (err) {
31
+ console.error("Snapshots fetch error:", err);
32
+ return NextResponse.json(
33
+ { error: err instanceof Error ? err.message : "Unknown error" },
34
+ { status: 500 }
35
+ );
36
+ }
37
+ }
@@ -0,0 +1,57 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+
3
+ /**
4
+ * Get enhanced transaction data from Helius for display in the activity feed.
5
+ * Uses the Enhanced Transactions API: POST /v0/transactions
6
+ */
7
+ export async function POST(req: NextRequest) {
8
+ try {
9
+ const { signatures } = await req.json();
10
+
11
+ if (!Array.isArray(signatures) || signatures.length === 0) {
12
+ return NextResponse.json({ error: "signatures array required" }, { status: 400 });
13
+ }
14
+
15
+ // Limit to 20 per request
16
+ const limited = signatures.slice(0, 20);
17
+ const apiKey = process.env.HELIUS_API_KEY;
18
+
19
+ const res = await fetch(
20
+ `https://api-mainnet.helius-rpc.com/v0/transactions?api-key=${apiKey}`,
21
+ {
22
+ method: "POST",
23
+ headers: { "Content-Type": "application/json" },
24
+ body: JSON.stringify({ transactions: limited }),
25
+ }
26
+ );
27
+
28
+ if (!res.ok) {
29
+ const error = await res.text();
30
+ return NextResponse.json({ error }, { status: res.status });
31
+ }
32
+
33
+ const data = await res.json();
34
+
35
+ // Return a simplified structure for the frontend
36
+ const enhanced = data.map((tx: Record<string, unknown>) => ({
37
+ signature: tx.signature,
38
+ type: tx.type,
39
+ source: tx.source,
40
+ description: tx.description,
41
+ fee: tx.fee,
42
+ feePayer: tx.feePayer,
43
+ timestamp: tx.timestamp,
44
+ nativeTransfers: tx.nativeTransfers,
45
+ tokenTransfers: tx.tokenTransfers,
46
+ swap: (tx.events as Record<string, unknown>)?.swap ?? null,
47
+ }));
48
+
49
+ return NextResponse.json({ transactions: enhanced });
50
+ } catch (err) {
51
+ console.error("Enhanced transactions error:", err);
52
+ return NextResponse.json(
53
+ { error: String(err) },
54
+ { status: 500 }
55
+ );
56
+ }
57
+ }