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,39 @@
1
+ @import "tailwindcss";
2
+
3
+ @keyframes activitySlideIn {
4
+ from {
5
+ opacity: 0;
6
+ transform: translateX(-12px);
7
+ }
8
+ to {
9
+ opacity: 1;
10
+ transform: translateX(0);
11
+ }
12
+ }
13
+
14
+ @keyframes fadeSlideUp {
15
+ from {
16
+ opacity: 0;
17
+ transform: translate(-50%, 20px);
18
+ }
19
+ to {
20
+ opacity: 1;
21
+ transform: translate(-50%, 0);
22
+ }
23
+ }
24
+
25
+ @keyframes scanLine {
26
+ 0% { top: 0%; opacity: 0; }
27
+ 10% { opacity: 1; }
28
+ 90% { opacity: 1; }
29
+ 100% { top: 100%; opacity: 0; }
30
+ }
31
+
32
+ @keyframes claimShimmer {
33
+ 0%, 100% {
34
+ background-position: 0% 50%;
35
+ }
36
+ 50% {
37
+ background-position: 100% 50%;
38
+ }
39
+ }
@@ -0,0 +1,43 @@
1
+ import type { Metadata } from "next";
2
+ import { Geist, Geist_Mono } from "next/font/google";
3
+ import { Analytics } from "@vercel/analytics/next";
4
+ import { AuthProvider } from "@/context/AuthContext";
5
+ import PhantomWrapper from "@/components/PhantomWrapper";
6
+ import ConvexWrapper from "@/components/ConvexWrapper";
7
+ import "./globals.css";
8
+
9
+ const geistSans = Geist({
10
+ variable: "--font-geist-sans",
11
+ subsets: ["latin"],
12
+ });
13
+
14
+ const geistMono = Geist_Mono({
15
+ variable: "--font-geist-mono",
16
+ subsets: ["latin"],
17
+ });
18
+
19
+ export const metadata: Metadata = {
20
+ title: "Solanapolis — The Solana City",
21
+ description: "Solanapolis — A real-time 3D city built on Solana wallets. Drive, fly, battle, and explore with multiplayer combat, beaches, and boats. Powered by Helius.",
22
+ };
23
+
24
+ export default function RootLayout({
25
+ children,
26
+ }: Readonly<{
27
+ children: React.ReactNode;
28
+ }>) {
29
+ return (
30
+ <html lang="en" className="w-full h-full overflow-hidden">
31
+ <body
32
+ className={`${geistSans.variable} ${geistMono.variable} antialiased w-full h-full overflow-hidden bg-[#0a0a12]`}
33
+ >
34
+ <PhantomWrapper>
35
+ <ConvexWrapper>
36
+ <AuthProvider>{children}</AuthProvider>
37
+ </ConvexWrapper>
38
+ </PhantomWrapper>
39
+ <Analytics />
40
+ </body>
41
+ </html>
42
+ );
43
+ }
@@ -0,0 +1,16 @@
1
+ "use client";
2
+
3
+ import dynamic from "next/dynamic";
4
+
5
+ const CityScene = dynamic(() => import("@/components/CityScene"), {
6
+ ssr: false,
7
+ loading: () => (
8
+ <div className="flex items-center justify-center h-screen bg-[#0a0a12] text-white/40 text-sm">
9
+ Loading city...
10
+ </div>
11
+ ),
12
+ });
13
+
14
+ export default function Home() {
15
+ return <CityScene />;
16
+ }
@@ -0,0 +1,206 @@
1
+ "use client";
2
+
3
+ import { useRef, useMemo, useEffect } from "react";
4
+ import { useQuery, useMutation } from "convex/react";
5
+ import { useFrame } from "@react-three/fiber";
6
+ import { Html } from "@react-three/drei";
7
+ import * as THREE from "three";
8
+ import { api } from "../../convex/_generated/api";
9
+ import { GRID_WORLD } from "../lib/city-constants";
10
+
11
+ /**
12
+ * AITownNPCs — renders AI Town agents as small walking figures in the 3D city.
13
+ *
14
+ * AI Town uses a tile-based map (gentle.js: 48×32 tiles).
15
+ * The 3D city spans GRID_WORLD (572) units centered at origin.
16
+ * We map tile coordinates → 3D world coordinates so NPCs walk the streets.
17
+ */
18
+
19
+ // AI Town map dimensions (from data/gentle.js)
20
+ const TILE_MAP_W = 48;
21
+ const TILE_MAP_H = 32;
22
+
23
+ // Map AI Town tile (x, y) → 3D world (X, Z)
24
+ // Tiles go 0..W-1, 0..H-1. We map to [-GRID_WORLD/2, +GRID_WORLD/2]
25
+ function tileToWorld(tileX: number, tileY: number): [number, number, number] {
26
+ const x = (tileX / TILE_MAP_W - 0.5) * GRID_WORLD;
27
+ const z = (tileY / TILE_MAP_H - 0.5) * GRID_WORLD;
28
+ return [x, 0.6, z]; // Y=0.6 — slightly above street level
29
+ }
30
+
31
+ // Deterministic color from name
32
+ function nameColor(name: string): string {
33
+ let h = 0;
34
+ for (let i = 0; i < name.length; i++) h = ((h << 5) - h + name.charCodeAt(i)) | 0;
35
+ return `hsl(${Math.abs(h) % 360}, 70%, 60%)`;
36
+ }
37
+
38
+ interface NPCData {
39
+ id: string;
40
+ name: string;
41
+ x: number;
42
+ y: number;
43
+ targetX: number;
44
+ targetY: number;
45
+ activity?: string;
46
+ speaking: boolean;
47
+ color: string;
48
+ }
49
+
50
+ export default function AITownNPCs() {
51
+ // Bail out if Convex isn't configured
52
+ if (!process.env.NEXT_PUBLIC_CONVEX_URL) return null;
53
+ return <AITownNPCsInner />;
54
+ }
55
+
56
+ function AITownNPCsInner() {
57
+ const worldStatus = useQuery(api.world.defaultWorldStatus);
58
+ const worldId = worldStatus?.worldId;
59
+
60
+ const worldState = useQuery(
61
+ api.world.worldState,
62
+ worldId ? { worldId } : "skip",
63
+ );
64
+ const descriptions = useQuery(
65
+ api.world.gameDescriptions,
66
+ worldId ? { worldId } : "skip",
67
+ );
68
+
69
+ // Send heartbeat to keep world alive
70
+ const heartbeatMutation = useMutation(api.world.heartbeatWorld);
71
+ useEffect(() => {
72
+ if (!worldId) return;
73
+ heartbeatMutation({ worldId });
74
+ const interval = setInterval(() => heartbeatMutation({ worldId }), 30_000);
75
+ return () => clearInterval(interval);
76
+ }, [worldId, heartbeatMutation]);
77
+
78
+ // Extract NPC data from world state
79
+ const npcs = useMemo<NPCData[]>(() => {
80
+ if (!worldState?.world || !descriptions) return [];
81
+
82
+ const world = worldState.world;
83
+ const descMap = new Map<string, { name: string; character: string }>();
84
+ for (const pd of descriptions.playerDescriptions) {
85
+ descMap.set(pd.playerId, { name: pd.name, character: pd.character });
86
+ }
87
+
88
+ // Find which players are in conversations (speaking)
89
+ const speakingPlayers = new Set<string>();
90
+ for (const conv of world.conversations ?? []) {
91
+ if (conv.isTyping?.playerId) {
92
+ speakingPlayers.add(conv.isTyping.playerId);
93
+ }
94
+ for (const p of conv.participants ?? []) {
95
+ if (p.status?.kind === "participating") {
96
+ speakingPlayers.add(p.playerId);
97
+ }
98
+ }
99
+ }
100
+
101
+ return (world.players ?? []).map((p: any) => {
102
+ const desc = descMap.get(p.id);
103
+ const name = desc?.name ?? `NPC-${p.id}`;
104
+ return {
105
+ id: p.id,
106
+ name,
107
+ x: p.position?.x ?? 0,
108
+ y: p.position?.y ?? 0,
109
+ targetX: p.pathfinding?.destination?.x ?? p.position?.x ?? 0,
110
+ targetY: p.pathfinding?.destination?.y ?? p.position?.y ?? 0,
111
+ activity: p.activity?.description,
112
+ speaking: speakingPlayers.has(p.id),
113
+ color: nameColor(name),
114
+ };
115
+ });
116
+ }, [worldState, descriptions]);
117
+
118
+ if (npcs.length === 0) return null;
119
+
120
+ return (
121
+ <group>
122
+ {npcs.map((npc) => (
123
+ <NPCFigure key={npc.id} npc={npc} />
124
+ ))}
125
+ </group>
126
+ );
127
+ }
128
+
129
+ /** Smooth-interpolated NPC figure with head + body + name label */
130
+ function NPCFigure({ npc }: { npc: NPCData }) {
131
+ const groupRef = useRef<THREE.Group>(null);
132
+ const currentPos = useRef<THREE.Vector3>(
133
+ new THREE.Vector3(...tileToWorld(npc.x, npc.y)),
134
+ );
135
+ const bobPhase = useRef(Math.random() * Math.PI * 2);
136
+
137
+ // Smoothly interpolate position each frame
138
+ useFrame((_, delta) => {
139
+ if (!groupRef.current) return;
140
+
141
+ const [tx, ty, tz] = tileToWorld(npc.x, npc.y);
142
+ const target = new THREE.Vector3(tx, ty, tz);
143
+ currentPos.current.lerp(target, Math.min(1, delta * 3));
144
+ groupRef.current.position.copy(currentPos.current);
145
+
146
+ // Walking bob
147
+ bobPhase.current += delta * 6;
148
+ const bob = Math.abs(Math.sin(bobPhase.current)) * 0.15;
149
+ groupRef.current.position.y = currentPos.current.y + bob;
150
+
151
+ // Face movement direction
152
+ const dx = target.x - groupRef.current.position.x;
153
+ const dz = target.z - groupRef.current.position.z;
154
+ if (Math.abs(dx) > 0.01 || Math.abs(dz) > 0.01) {
155
+ groupRef.current.rotation.y = Math.atan2(dx, dz);
156
+ }
157
+ });
158
+
159
+ const bodyColor = useMemo(() => new THREE.Color(npc.color), [npc.color]);
160
+
161
+ return (
162
+ <group ref={groupRef}>
163
+ {/* Body — capsule shape */}
164
+ <mesh position={[0, 0.6, 0]} castShadow>
165
+ <capsuleGeometry args={[0.3, 0.6, 4, 8]} />
166
+ <meshStandardMaterial color={bodyColor} roughness={0.6} metalness={0.2} />
167
+ </mesh>
168
+
169
+ {/* Head */}
170
+ <mesh position={[0, 1.3, 0]} castShadow>
171
+ <sphereGeometry args={[0.25, 8, 8]} />
172
+ <meshStandardMaterial color={bodyColor} roughness={0.5} metalness={0.1} />
173
+ </mesh>
174
+
175
+ {/* Speaking indicator — glowing ring */}
176
+ {npc.speaking && (
177
+ <mesh position={[0, 1.7, 0]} rotation={[Math.PI / 2, 0, 0]}>
178
+ <torusGeometry args={[0.2, 0.04, 8, 16]} />
179
+ <meshBasicMaterial color="#fbbf24" transparent opacity={0.8} />
180
+ </mesh>
181
+ )}
182
+
183
+ {/* Activity emoji */}
184
+ {npc.activity && (
185
+ <Html center position={[0, 2.0, 0]} distanceFactor={18} style={{ pointerEvents: "none" }}>
186
+ <div className="text-sm select-none">{npc.activity}</div>
187
+ </Html>
188
+ )}
189
+
190
+ {/* Name tag */}
191
+ <Html center position={[0, 1.8, 0]} distanceFactor={16} style={{ pointerEvents: "none" }}>
192
+ <div className="px-1.5 py-0.5 rounded bg-black/70 border border-white/10 whitespace-nowrap backdrop-blur-sm">
193
+ <span
194
+ className="text-[8px] font-bold"
195
+ style={{ color: npc.color }}
196
+ >
197
+ {npc.name}
198
+ </span>
199
+ {npc.speaking && (
200
+ <span className="text-[7px] text-amber-300 ml-1 animate-pulse">💬</span>
201
+ )}
202
+ </div>
203
+ </Html>
204
+ </group>
205
+ );
206
+ }
@@ -0,0 +1,189 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState, useRef } from "react";
4
+ import { PlacedWallet } from "@/types/wallet";
5
+
6
+ interface ActivityEvent {
7
+ id: string;
8
+ walletAddress: string;
9
+ signature: string;
10
+ tokenIn: string | null;
11
+ tokenOut: string | null;
12
+ amountSol: number | null;
13
+ createdAt: string;
14
+ age: number; // seconds ago
15
+ }
16
+
17
+ const MAX_EVENTS = 8;
18
+ const POLL_INTERVAL = 5000;
19
+
20
+ // Known token symbols
21
+ const TOKEN_SYMBOLS: Record<string, string> = {
22
+ So11111111111111111111111111111111: "SOL",
23
+ EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v: "USDC",
24
+ Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB: "USDT",
25
+ DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263: "BONK",
26
+ JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN: "JUP",
27
+ mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So: "mSOL",
28
+ DUSTawucrTsGU8hcqRdHDCbuYhCPADMLM2VcCb8VnFnQ: "DUST",
29
+ "7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs": "ETH",
30
+ };
31
+
32
+ function shortenAddress(addr: string): string {
33
+ return `${addr.slice(0, 4)}…${addr.slice(-4)}`;
34
+ }
35
+
36
+ function tokenLabel(mint: string | null): string {
37
+ if (!mint) return "???";
38
+ return TOKEN_SYMBOLS[mint] ?? shortenAddress(mint);
39
+ }
40
+
41
+ function formatAge(seconds: number): string {
42
+ if (seconds < 60) return `${seconds}s ago`;
43
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
44
+ return `${Math.floor(seconds / 3600)}h ago`;
45
+ }
46
+
47
+ interface ActivityFeedProps {
48
+ wallets: PlacedWallet[];
49
+ onClickAddress?: (address: string) => void;
50
+ }
51
+
52
+ export default function ActivityFeed({ wallets, onClickAddress }: ActivityFeedProps) {
53
+ const [events, setEvents] = useState<ActivityEvent[]>([]);
54
+ const [visible, setVisible] = useState(true);
55
+ const tickRef = useRef(0);
56
+
57
+ useEffect(() => {
58
+ if (wallets.length === 0) return;
59
+
60
+ let cancelled = false;
61
+
62
+ async function fetchLatest() {
63
+ try {
64
+ const { createClient } = await import("@/lib/supabase");
65
+ const supabase = createClient();
66
+
67
+ const { data } = await supabase
68
+ .from("swap_events")
69
+ .select("*")
70
+ .order("created_at", { ascending: false })
71
+ .limit(MAX_EVENTS);
72
+
73
+ if (cancelled || !data) return;
74
+
75
+ const now = Date.now();
76
+ const mapped: ActivityEvent[] = data.map((row: Record<string, unknown>) => ({
77
+ id: `${row.signature}-${row.created_at}`,
78
+ walletAddress: row.wallet_address as string,
79
+ signature: row.signature as string,
80
+ tokenIn: (row.token_in as string) ?? null,
81
+ tokenOut: (row.token_out as string) ?? null,
82
+ amountSol: row.amount_sol != null ? Number(row.amount_sol) : null,
83
+ createdAt: row.created_at as string,
84
+ age: Math.floor((now - new Date(row.created_at as string).getTime()) / 1000),
85
+ }));
86
+
87
+ setEvents(mapped);
88
+ } catch {
89
+ // silently fail
90
+ }
91
+ }
92
+
93
+ fetchLatest();
94
+ const interval = setInterval(() => {
95
+ fetchLatest();
96
+ tickRef.current++;
97
+ }, POLL_INTERVAL);
98
+
99
+ return () => {
100
+ cancelled = true;
101
+ clearInterval(interval);
102
+ };
103
+ }, [wallets]);
104
+
105
+ // Update ages every second
106
+ useEffect(() => {
107
+ const interval = setInterval(() => {
108
+ setEvents((prev) =>
109
+ prev.map((e) => ({
110
+ ...e,
111
+ age: Math.floor((Date.now() - new Date(e.createdAt).getTime()) / 1000),
112
+ }))
113
+ );
114
+ }, 1000);
115
+ return () => clearInterval(interval);
116
+ }, []);
117
+
118
+ if (events.length === 0) return null;
119
+
120
+ return (
121
+ <div className="absolute top-[8.5rem] sm:top-20 left-3 sm:left-5 z-10 w-72">
122
+ <button
123
+ onClick={() => setVisible(!visible)}
124
+ className="flex items-center gap-2 mb-1.5 px-2.5 py-1 bg-black/50 backdrop-blur-xl border border-white/[0.08] rounded-full text-xs text-white/50 hover:text-white/70 transition-colors cursor-pointer"
125
+ >
126
+ <span
127
+ className="w-1.5 h-1.5 rounded-full animate-pulse"
128
+ style={{ backgroundColor: "#22c55e" }}
129
+ />
130
+ Live Activity
131
+ <svg
132
+ width="8"
133
+ height="8"
134
+ viewBox="0 0 8 8"
135
+ fill="none"
136
+ stroke="currentColor"
137
+ strokeWidth="1.5"
138
+ className={`transition-transform ${visible ? "rotate-180" : ""}`}
139
+ >
140
+ <path d="M1 3l3 3 3-3" />
141
+ </svg>
142
+ </button>
143
+
144
+ {visible && (
145
+ <div className="bg-black/50 backdrop-blur-xl border border-white/[0.08] rounded-2xl overflow-hidden">
146
+ {events.map((evt, i) => (
147
+ <button
148
+ key={evt.id}
149
+ onClick={() => onClickAddress?.(evt.walletAddress)}
150
+ className={`w-full text-left px-3 py-2 hover:bg-white/[0.04] transition-colors cursor-pointer ${
151
+ i < events.length - 1 ? "border-b border-white/[0.04]" : ""
152
+ }`}
153
+ style={{
154
+ animation: `activitySlideIn 0.3s ease-out ${i * 0.05}s both`,
155
+ }}
156
+ >
157
+ <div className="flex items-center justify-between gap-2">
158
+ <div className="flex items-center gap-1.5 min-w-0">
159
+ <span className="text-xs font-mono text-purple-300/70 shrink-0">
160
+ {shortenAddress(evt.walletAddress)}
161
+ </span>
162
+ <span className="text-[10px] text-white/25">swapped</span>
163
+ </div>
164
+ <span className="text-[10px] text-white/20 shrink-0">
165
+ {formatAge(evt.age)}
166
+ </span>
167
+ </div>
168
+ <div className="flex items-center gap-1 mt-0.5">
169
+ <span className="text-[11px] text-red-300/60">{tokenLabel(evt.tokenIn)}</span>
170
+ <svg width="8" height="6" viewBox="0 0 8 6" fill="none" className="shrink-0 text-white/20">
171
+ <path d="M1 3h6M5 1l2 2-2 2" stroke="currentColor" strokeWidth="1" />
172
+ </svg>
173
+ <span className="text-[11px] text-green-300/60">{tokenLabel(evt.tokenOut)}</span>
174
+ {evt.amountSol != null && (
175
+ <span className="text-[10px] text-white/20 ml-auto">
176
+ {evt.amountSol < 0.01
177
+ ? "<0.01"
178
+ : evt.amountSol.toFixed(2)}{" "}
179
+ SOL
180
+ </span>
181
+ )}
182
+ </div>
183
+ </button>
184
+ ))}
185
+ </div>
186
+ )}
187
+ </div>
188
+ );
189
+ }
@@ -0,0 +1,163 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { useAuth } from "@/context/AuthContext";
5
+
6
+
7
+ interface AuthPanelProps {
8
+ onClickAddress?: (address: string) => void;
9
+ }
10
+
11
+ export default function AuthPanel({ onClickAddress }: AuthPanelProps) {
12
+ const { user, profile, loading, connectPhantom, connectX, linkWallet, signOut, phantomAddress, phantomConnected, openPhantomModal } =
13
+ useAuth();
14
+ const [error, setError] = useState<string | null>(null);
15
+ const [connecting, setConnecting] = useState(false);
16
+
17
+ if (loading) return null;
18
+
19
+ const phantomSection = (
20
+ <div className="flex items-center gap-2">
21
+ {phantomConnected && phantomAddress ? (
22
+ <>
23
+ <span className="w-1.5 h-1.5 rounded-full bg-green-400 animate-pulse" />
24
+ <span className="font-mono text-xs text-purple-300/80">
25
+ {phantomAddress.slice(0, 4)}...{phantomAddress.slice(-4)}
26
+ </span>
27
+ <span className="text-white/30 text-xs">✈ Ready to fly</span>
28
+ </>
29
+ ) : (
30
+ <button
31
+ onClick={openPhantomModal}
32
+ className="flex items-center gap-1.5 px-3 py-1.5 bg-purple-500/15 hover:bg-purple-500/25 border border-purple-400/20 rounded-xl text-sm text-purple-200 transition-colors cursor-pointer"
33
+ >
34
+ <PhantomIcon />
35
+ <span className="hidden sm:inline">Connect &amp; Fly</span>
36
+ </button>
37
+ )}
38
+ </div>
39
+ );
40
+
41
+ async function handlePhantom() {
42
+ setError(null);
43
+ setConnecting(true);
44
+ try {
45
+ await connectPhantom();
46
+ } catch (err) {
47
+ setError(err instanceof Error ? err.message : "Failed to connect");
48
+ } finally {
49
+ setConnecting(false);
50
+ }
51
+ }
52
+
53
+ async function handleX() {
54
+ setError(null);
55
+ try {
56
+ await connectX();
57
+ } catch (err) {
58
+ setError(err instanceof Error ? err.message : "Failed to connect X");
59
+ }
60
+ }
61
+
62
+ async function handleLinkWallet() {
63
+ setError(null);
64
+ setConnecting(true);
65
+ try {
66
+ await linkWallet();
67
+ } catch (err) {
68
+ setError(err instanceof Error ? err.message : "Failed to link wallet");
69
+ } finally {
70
+ setConnecting(false);
71
+ }
72
+ }
73
+
74
+ // When not signed in, just show the phantom connect section
75
+ if (!user) {
76
+ return (
77
+ <div className="flex flex-wrap items-center gap-3 bg-black/50 backdrop-blur-xl border border-white/[0.08] rounded-2xl px-4 py-2.5">
78
+ {phantomSection}
79
+ {error && <p className="text-red-400 text-sm ml-2">{error}</p>}
80
+ </div>
81
+ );
82
+ }
83
+
84
+ return (
85
+ <div className="flex flex-wrap items-center gap-3 bg-black/50 backdrop-blur-xl border border-white/[0.08] rounded-2xl px-4 py-2.5">
86
+ {phantomSection}
87
+ {profile?.wallet_address && (
88
+ <div className="flex items-center gap-2">
89
+ <span className="w-1.5 h-1.5 rounded-full bg-green-400" />
90
+ <button
91
+ onClick={() => onClickAddress?.(profile.wallet_address!)}
92
+ className="hidden sm:inline font-mono text-sm text-purple-300/80 hover:text-purple-200 transition-colors cursor-pointer"
93
+ >
94
+ {profile.wallet_address.slice(0, 4)}...
95
+ {profile.wallet_address.slice(-4)}
96
+ </button>
97
+ </div>
98
+ )}
99
+ {profile?.x_username && (
100
+ <div className="flex items-center gap-2">
101
+ {profile.x_avatar_url && (
102
+ <img
103
+ src={profile.x_avatar_url}
104
+ alt=""
105
+ className="w-5 h-5 rounded-full"
106
+ />
107
+ )}
108
+ <span className="hidden sm:inline text-sm text-blue-300">@{profile.x_username}</span>
109
+ </div>
110
+ )}
111
+ <div className="flex items-center gap-2">
112
+ {!profile?.wallet_address && (
113
+ <button
114
+ onClick={handleLinkWallet}
115
+ disabled={connecting}
116
+ className="flex items-center gap-1.5 px-2.5 sm:px-3 py-1.5 bg-purple-500/15 hover:bg-purple-500/25 border border-purple-400/20 rounded-xl text-sm text-purple-200 transition-colors disabled:opacity-50 cursor-pointer"
117
+ >
118
+ <PhantomIcon />
119
+ <span className="hidden sm:inline">{connecting ? "..." : "Link Wallet"}</span>
120
+ </button>
121
+ )}
122
+ {!profile?.x_username && (
123
+ <button
124
+ onClick={handleX}
125
+ className="flex items-center gap-1.5 px-2.5 sm:px-3 py-1.5 bg-white/[0.04] hover:bg-white/[0.08] border border-white/[0.08] rounded-xl text-sm text-white/60 transition-colors cursor-pointer"
126
+ >
127
+ <XIcon />
128
+ <span className="hidden sm:inline">Link X</span>
129
+ </button>
130
+ )}
131
+ <button
132
+ onClick={signOut}
133
+ className="px-2 py-1 text-sm text-white/25 hover:text-white/50 transition-colors cursor-pointer"
134
+ >
135
+ <span className="hidden sm:inline">Disconnect</span>
136
+ <span className="sm:hidden">
137
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
138
+ <path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4" /><polyline points="16 17 21 12 16 7" /><line x1="21" y1="12" x2="9" y2="12" />
139
+ </svg>
140
+ </span>
141
+ </button>
142
+ </div>
143
+ {error && <p className="text-red-400 text-sm ml-2">{error}</p>}
144
+ </div>
145
+ );
146
+ }
147
+
148
+ function PhantomIcon() {
149
+ return (
150
+ <svg width="16" height="16" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
151
+ <rect width="128" height="128" rx="26" fill="#AB9FF2" />
152
+ <path fillRule="evenodd" clipRule="evenodd" d="M55.6416 82.1477C50.8744 89.4525 42.8862 98.6966 32.2568 98.6966C27.232 98.6966 22.4004 96.628 22.4004 87.6424C22.4004 64.7584 53.6445 29.3335 82.6339 29.3335C99.1257 29.3335 105.697 40.7755 105.697 53.7689C105.697 70.4471 94.8739 89.5171 84.1156 89.5171C80.7013 89.5171 79.0264 87.6424 79.0264 84.6688C79.0264 83.8931 79.1552 83.0527 79.4129 82.1477C75.7409 88.4182 68.6546 94.2361 62.0192 94.2361C57.1877 94.2361 54.7397 91.1979 54.7397 86.9314C54.7397 85.3799 55.0618 83.7638 55.6416 82.1477ZM80.6133 53.3182C80.6133 57.1044 78.3795 58.9975 75.8806 58.9975C73.3438 58.9975 71.1479 57.1044 71.1479 53.3182C71.1479 49.532 73.3438 47.6389 75.8806 47.6389C78.3795 47.6389 80.6133 49.532 80.6133 53.3182ZM94.8102 53.3184C94.8102 57.1046 92.5763 58.9977 90.0775 58.9977C87.5407 58.9977 85.3447 57.1046 85.3447 53.3184C85.3447 49.5323 87.5407 47.6392 90.0775 47.6392C92.5763 47.6392 94.8102 49.5323 94.8102 53.3184Z" fill="#FFFDF8" />
153
+ </svg>
154
+ );
155
+ }
156
+
157
+ function XIcon() {
158
+ return (
159
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
160
+ <path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
161
+ </svg>
162
+ );
163
+ }