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,83 @@
1
+ import { NextResponse } from "next/server";
2
+
3
+ /**
4
+ * Treasury balance API — fetches SOL balance of the reward treasury wallet.
5
+ * Primary: Helius RPC getBalance (reliable, fast)
6
+ * Also fetches SOL price from Helius Wallet API for USD display
7
+ */
8
+
9
+ const TREASURY_ADDRESS = "76QQqndM87DBQAeAnRnjdMSiVBgBNrm55HMa88MRVc7p";
10
+ const REWARD_PER_PARCEL = 0.069420;
11
+
12
+ export async function GET() {
13
+ try {
14
+ const apiKey = process.env.HELIUS_API_KEY;
15
+ const rpcUrl = process.env.HELIUS_RPC_URL || `https://mainnet.helius-rpc.com/?api-key=${apiKey}`;
16
+
17
+ // Primary: getBalance via RPC (most reliable)
18
+ const balRes = await fetch(rpcUrl, {
19
+ method: "POST",
20
+ headers: { "Content-Type": "application/json" },
21
+ body: JSON.stringify({
22
+ jsonrpc: "2.0",
23
+ id: "treasury-bal",
24
+ method: "getBalance",
25
+ params: [TREASURY_ADDRESS],
26
+ }),
27
+ });
28
+
29
+ const balData = await balRes.json();
30
+ const lamports = balData?.result?.value ?? 0;
31
+ const sol = lamports / 1_000_000_000;
32
+
33
+ // Try to get SOL price from Helius Wallet API for USD display
34
+ let solPrice: number | null = null;
35
+ let solUsdValue: number | null = null;
36
+
37
+ if (apiKey) {
38
+ try {
39
+ const walletRes = await fetch(
40
+ `https://api.helius.xyz/v1/wallet/${TREASURY_ADDRESS}/balances?api-key=${apiKey}&showNative=true`,
41
+ { signal: AbortSignal.timeout(5000) },
42
+ );
43
+ if (walletRes.ok) {
44
+ const wData = await walletRes.json();
45
+ const solEntry = wData.balances?.find(
46
+ (b: { mint: string }) => b.mint === "So11111111111111111111111111111111111111112",
47
+ );
48
+ if (solEntry) {
49
+ solPrice = solEntry.pricePerToken ?? null;
50
+ solUsdValue = solEntry.usdValue ?? null;
51
+ }
52
+ }
53
+ } catch {
54
+ // Wallet API failed — still have balance from RPC, just no USD price
55
+ }
56
+ }
57
+
58
+ // Calculate how many 0.069420 SOL parcels can still be funded
59
+ const TX_FEE = 0.000005;
60
+ const parcelsRemaining = Math.floor(sol / (REWARD_PER_PARCEL + TX_FEE));
61
+
62
+ return NextResponse.json({
63
+ address: TREASURY_ADDRESS,
64
+ balanceSOL: Math.round(sol * 1_000_000) / 1_000_000,
65
+ balanceLamports: lamports,
66
+ solUsdValue,
67
+ solPrice,
68
+ parcelsRemaining,
69
+ rewardPerParcel: REWARD_PER_PARCEL,
70
+ source: "helius-rpc",
71
+ }, {
72
+ headers: { "Cache-Control": "public, max-age=15" },
73
+ });
74
+ } catch (err) {
75
+ console.error("Treasury balance error:", err);
76
+ return NextResponse.json({
77
+ address: TREASURY_ADDRESS,
78
+ balanceSOL: 0,
79
+ parcelsRemaining: 0,
80
+ error: "Failed to fetch balance",
81
+ }, { status: 500 });
82
+ }
83
+ }
@@ -0,0 +1,124 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+
3
+ const HELIUS_API_KEY = process.env.HELIUS_API_KEY!;
4
+ const HELIUS_RPC_URL = `https://mainnet.helius-rpc.com/?api-key=${HELIUS_API_KEY}`;
5
+
6
+ interface TokenInfo {
7
+ mint: string;
8
+ symbol?: string;
9
+ name?: string;
10
+ description?: string;
11
+ image?: string;
12
+ amount: number;
13
+ decimals: number;
14
+ uiAmount: number;
15
+ price?: number;
16
+ value?: number;
17
+ supply?: number;
18
+ marketCap?: number;
19
+ }
20
+
21
+ export async function GET(
22
+ _req: NextRequest,
23
+ { params }: { params: Promise<{ address: string }> },
24
+ ) {
25
+ const { address } = await params;
26
+
27
+ if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(address)) {
28
+ return NextResponse.json(
29
+ { error: "Invalid Solana address" },
30
+ { status: 400 },
31
+ );
32
+ }
33
+
34
+ try {
35
+ // Single DAS call to get all fungible tokens with metadata, prices, and native balance
36
+ const dasRes = await fetch(HELIUS_RPC_URL, {
37
+ method: "POST",
38
+ headers: { "Content-Type": "application/json" },
39
+ body: JSON.stringify({
40
+ jsonrpc: "2.0",
41
+ id: 1,
42
+ method: "getAssetsByOwner",
43
+ params: {
44
+ ownerAddress: address,
45
+ page: 1,
46
+ limit: 100,
47
+ displayOptions: {
48
+ showFungible: true,
49
+ showNativeBalance: true,
50
+ },
51
+ },
52
+ }),
53
+ });
54
+
55
+ const dasJson = await dasRes.json();
56
+ if (dasJson.error) {
57
+ throw new Error(`DAS error: ${dasJson.error.message}`);
58
+ }
59
+
60
+ const result = dasJson.result;
61
+ const solBalance = (result.nativeBalance?.lamports ?? 0) / 1e9;
62
+
63
+ // Filter to fungible tokens with balance > 0
64
+ const tokens: TokenInfo[] = [];
65
+ for (const item of result.items ?? []) {
66
+ const isFungible =
67
+ item.interface === "FungibleToken" || item.interface === "FungibleAsset";
68
+ if (!isFungible) continue;
69
+
70
+ const ti = item.token_info;
71
+ if (!ti || !ti.balance || ti.balance <= 0) continue;
72
+
73
+ const decimals = ti.decimals ?? 0;
74
+ const uiAmount = ti.balance / Math.pow(10, decimals);
75
+ const price = ti.price_info?.price_per_token ?? undefined;
76
+ const supply = ti.supply != null
77
+ ? ti.supply / Math.pow(10, decimals)
78
+ : undefined;
79
+
80
+ tokens.push({
81
+ mint: item.id,
82
+ symbol:
83
+ item.content?.metadata?.symbol ||
84
+ ti.symbol ||
85
+ undefined,
86
+ name: item.content?.metadata?.name || undefined,
87
+ description: item.content?.metadata?.description || undefined,
88
+ image:
89
+ item.content?.links?.image ||
90
+ item.content?.files?.[0]?.cdn_uri ||
91
+ item.content?.files?.[0]?.uri ||
92
+ undefined,
93
+ amount: ti.balance,
94
+ decimals,
95
+ uiAmount,
96
+ price,
97
+ value: price != null ? uiAmount * price : undefined,
98
+ supply,
99
+ marketCap:
100
+ price != null && supply != null ? supply * price : undefined,
101
+ });
102
+ }
103
+
104
+ // Sort by value (USD) descending, fall back to uiAmount
105
+ tokens.sort((a, b) => (b.value ?? 0) - (a.value ?? 0) || b.uiAmount - a.uiAmount);
106
+
107
+ return NextResponse.json(
108
+ {
109
+ solBalance,
110
+ tokenCount: tokens.length,
111
+ tokens: tokens.slice(0, 20),
112
+ },
113
+ {
114
+ headers: { "Cache-Control": "public, max-age=60" },
115
+ },
116
+ );
117
+ } catch (err) {
118
+ console.error("Balance fetch error:", err);
119
+ return NextResponse.json(
120
+ { error: err instanceof Error ? err.message : "Unknown error" },
121
+ { status: 500 },
122
+ );
123
+ }
124
+ }
@@ -0,0 +1,32 @@
1
+ import { NextResponse } from "next/server";
2
+ import { createAdminClient } from "@/lib/supabase-admin";
3
+
4
+ export async function GET(
5
+ _req: Request,
6
+ { params }: { params: Promise<{ address: string }> },
7
+ ) {
8
+ const { address } = await params;
9
+ const supabase = createAdminClient();
10
+
11
+ // Read-only — return cached identity, no writes
12
+ const { data: row } = await supabase
13
+ .from("wallets")
14
+ .select("identity_name, identity_type, identity_category")
15
+ .eq("address", address)
16
+ .single();
17
+
18
+ if (row?.identity_name) {
19
+ return NextResponse.json({
20
+ identity: {
21
+ name: row.identity_name,
22
+ type: row.identity_type ?? "unknown",
23
+ category: row.identity_category ?? "",
24
+ },
25
+ });
26
+ }
27
+
28
+ return NextResponse.json(
29
+ { identity: null },
30
+ { headers: { "Cache-Control": "public, s-maxage=86400, stale-while-revalidate=86400" } },
31
+ );
32
+ }
@@ -0,0 +1,216 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { WalletStats, getWalletIdentity, getWalletFunding } from "@/lib/helius";
3
+ import { createAdminClient } from "@/lib/supabase-admin";
4
+
5
+ /**
6
+ * GET /api/wallet/[address]
7
+ *
8
+ * If the wallet exists in the DB, return its stats.
9
+ * If NOT, auto-ingest it using Helius APIs (Enhanced Transactions + Identity),
10
+ * place it in the city grid, and return the result.
11
+ */
12
+ export async function GET(
13
+ _req: NextRequest,
14
+ { params }: { params: Promise<{ address: string }> },
15
+ ) {
16
+ const { address } = await params;
17
+
18
+ if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(address)) {
19
+ return NextResponse.json(
20
+ { error: "Invalid Solana address" },
21
+ { status: 400 },
22
+ );
23
+ }
24
+
25
+ try {
26
+ const supabase = createAdminClient();
27
+
28
+ // Check if wallet already exists
29
+ const { data: existing, error: selectError } = await supabase
30
+ .from("wallets")
31
+ .select(
32
+ "address, txn_count, volume_traded_sol, fees_paid_sol, wallet_age_days, first_tx_at, ingestion_status, unique_tokens_swapped, latest_tx_at",
33
+ )
34
+ .eq("address", address)
35
+ .single();
36
+
37
+ if (selectError && selectError.code !== "PGRST116") {
38
+ throw new Error(`DB error: ${selectError.message}`);
39
+ }
40
+
41
+ // Wallet already in the city — return its stats
42
+ if (existing) {
43
+ const stats: WalletStats = {
44
+ address: existing.address,
45
+ txnCount: existing.txn_count,
46
+ walletAgeDays: existing.wallet_age_days,
47
+ volumeTraded: existing.volume_traded_sol,
48
+ feesPaid: existing.fees_paid_sol,
49
+ firstTxTimestamp: existing.first_tx_at
50
+ ? Math.floor(new Date(existing.first_tx_at).getTime() / 1000)
51
+ : null,
52
+ ingestionStatus: existing.ingestion_status,
53
+ uniqueTokensSwapped: existing.unique_tokens_swapped,
54
+ latestBlocktime: existing.latest_tx_at
55
+ ? Math.floor(new Date(existing.latest_tx_at).getTime() / 1000)
56
+ : null,
57
+ };
58
+ return NextResponse.json(stats);
59
+ }
60
+
61
+ // --- NEW WALLET: Auto-ingest using Helius APIs ---
62
+
63
+ // Check 1,000 wallet cap
64
+ const { count: walletCount } = await supabase
65
+ .from("wallets")
66
+ .select("*", { count: "exact", head: true });
67
+
68
+ if ((walletCount ?? 0) >= 1000) {
69
+ return NextResponse.json(
70
+ { error: "City is full! Only 1,000 spots available." },
71
+ { status: 429 },
72
+ );
73
+ }
74
+
75
+ const rpcUrl = process.env.HELIUS_RPC_URL;
76
+ if (!rpcUrl) {
77
+ return NextResponse.json(
78
+ { error: "RPC not configured" },
79
+ { status: 500 },
80
+ );
81
+ }
82
+
83
+ // Fetch transaction signatures to get real stats
84
+ const [sigRes, identityResult, fundingResult] = await Promise.all([
85
+ fetch(rpcUrl, {
86
+ method: "POST",
87
+ headers: { "Content-Type": "application/json" },
88
+ body: JSON.stringify({
89
+ jsonrpc: "2.0",
90
+ id: "sigs",
91
+ method: "getSignaturesForAddress",
92
+ params: [address, { limit: 1000 }],
93
+ }),
94
+ }),
95
+ getWalletIdentity(address).catch(() => null),
96
+ getWalletFunding(address).catch(() => null),
97
+ ]);
98
+
99
+ const sigData = await sigRes.json();
100
+ const sigs = sigData.result ?? [];
101
+
102
+ if (sigs.length === 0) {
103
+ return NextResponse.json(
104
+ { error: "No transactions found for this wallet" },
105
+ { status: 404 },
106
+ );
107
+ }
108
+
109
+ // Calculate basic stats from signatures
110
+ const txnCount = sigs.length;
111
+ const now = Date.now() / 1000;
112
+
113
+ // Oldest and newest blocktimes
114
+ const blocktimes = sigs
115
+ .map((s: { blockTime: number | null }) => s.blockTime)
116
+ .filter((bt: number | null): bt is number => bt !== null);
117
+
118
+ const oldestBlocktime = blocktimes.length > 0 ? Math.min(...blocktimes) : null;
119
+ const newestBlocktime = blocktimes.length > 0 ? Math.max(...blocktimes) : null;
120
+
121
+ // Wallet age from funding info or oldest tx
122
+ let firstTxAt: Date | null = null;
123
+ let walletAgeDays = 0;
124
+
125
+ if (fundingResult?.timestamp) {
126
+ firstTxAt = new Date(fundingResult.timestamp * 1000);
127
+ walletAgeDays = Math.floor((now - fundingResult.timestamp) / 86400);
128
+ } else if (oldestBlocktime) {
129
+ firstTxAt = new Date(oldestBlocktime * 1000);
130
+ walletAgeDays = Math.floor((now - oldestBlocktime) / 86400);
131
+ }
132
+
133
+ const latestTxAt = newestBlocktime ? new Date(newestBlocktime * 1000) : null;
134
+
135
+ // Estimate volume from SOL balance check
136
+ const balRes = await fetch(rpcUrl, {
137
+ method: "POST",
138
+ headers: { "Content-Type": "application/json" },
139
+ body: JSON.stringify({
140
+ jsonrpc: "2.0",
141
+ id: "bal",
142
+ method: "getBalance",
143
+ params: [address],
144
+ }),
145
+ });
146
+ const balData = await balRes.json();
147
+ const solBalance = (balData.result?.value ?? 0) / 1e9;
148
+
149
+ // Rough volume estimate based on tx count and balance
150
+ const volumeEstimate = solBalance + txnCount * 0.05;
151
+ const feesEstimate = txnCount * 0.000005;
152
+
153
+ // Get token accounts count as unique tokens estimate
154
+ const tokenRes = await fetch(rpcUrl, {
155
+ method: "POST",
156
+ headers: { "Content-Type": "application/json" },
157
+ body: JSON.stringify({
158
+ jsonrpc: "2.0",
159
+ id: "tokens",
160
+ method: "getTokenAccountsByOwner",
161
+ params: [
162
+ address,
163
+ { programId: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" },
164
+ { encoding: "jsonParsed" },
165
+ ],
166
+ }),
167
+ });
168
+ const tokenData = await tokenRes.json();
169
+ const uniqueTokens = tokenData.result?.value?.length ?? 0;
170
+
171
+ // Insert the wallet
172
+ const { error: insertError } = await supabase.from("wallets").upsert({
173
+ address,
174
+ txn_count: txnCount,
175
+ volume_traded_sol: Number(volumeEstimate.toFixed(4)),
176
+ fees_paid_sol: Number(feesEstimate.toFixed(6)),
177
+ wallet_age_days: walletAgeDays,
178
+ first_tx_at: firstTxAt?.toISOString() ?? null,
179
+ latest_tx_at: latestTxAt?.toISOString() ?? null,
180
+ ingestion_status: "complete",
181
+ unique_tokens_swapped: uniqueTokens,
182
+ identity_name: identityResult?.name ?? null,
183
+ identity_type: identityResult?.type ?? null,
184
+ identity_category: identityResult?.category ?? null,
185
+ updated_at: new Date().toISOString(),
186
+ }, { onConflict: "address" });
187
+
188
+ if (insertError) {
189
+ throw new Error(`Insert error: ${insertError.message}`);
190
+ }
191
+
192
+ // Assign a city position
193
+ await supabase.rpc("assign_city_position", { p_address: address });
194
+
195
+ // Return the new wallet stats
196
+ const stats: WalletStats = {
197
+ address,
198
+ txnCount,
199
+ walletAgeDays,
200
+ volumeTraded: Number(volumeEstimate.toFixed(4)),
201
+ feesPaid: Number(feesEstimate.toFixed(6)),
202
+ firstTxTimestamp: oldestBlocktime ?? (fundingResult?.timestamp ?? null),
203
+ ingestionStatus: "complete",
204
+ uniqueTokensSwapped: uniqueTokens,
205
+ latestBlocktime: newestBlocktime,
206
+ };
207
+
208
+ return NextResponse.json({ ...stats, isNew: true });
209
+ } catch (err) {
210
+ console.error("Wallet fetch error:", err);
211
+ return NextResponse.json(
212
+ { error: err instanceof Error ? err.message : "Unknown error" },
213
+ { status: 500 },
214
+ );
215
+ }
216
+ }
@@ -0,0 +1,41 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { createAdminClient } from "@/lib/supabase-admin";
3
+
4
+ export async function GET(
5
+ _req: NextRequest,
6
+ { params }: { params: Promise<{ address: string }> },
7
+ ) {
8
+ const { address } = await params;
9
+
10
+ if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(address)) {
11
+ return NextResponse.json(
12
+ { error: "Invalid Solana address" },
13
+ { status: 400 },
14
+ );
15
+ }
16
+
17
+ try {
18
+ const supabase = createAdminClient();
19
+
20
+ const { data: wallet, error } = await supabase
21
+ .from("wallets")
22
+ .select("traded_token_mints")
23
+ .eq("address", address)
24
+ .single();
25
+
26
+ if (error && error.code !== "PGRST116") {
27
+ throw new Error(`DB error: ${error.message}`);
28
+ }
29
+
30
+ return NextResponse.json(
31
+ { mints: wallet?.traded_token_mints ?? [] },
32
+ { headers: { "Cache-Control": "public, max-age=300" } },
33
+ );
34
+ } catch (err) {
35
+ console.error("Traded tokens fetch error:", err);
36
+ return NextResponse.json(
37
+ { error: err instanceof Error ? err.message : "Unknown error" },
38
+ { status: 500 },
39
+ );
40
+ }
41
+ }
@@ -0,0 +1,68 @@
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
+ // Self-heal: assign positions to any unplaced complete wallets
9
+ await supabase.rpc("repair_unplaced_wallets");
10
+
11
+ const [walletsResult, profilesResult] = await Promise.all([
12
+ supabase
13
+ .from("wallets")
14
+ .select(
15
+ "address, txn_count, volume_traded_sol, fees_paid_sol, wallet_age_days, block_row, block_col, local_slot, unique_tokens_swapped, latest_tx_at, identity_name, identity_type, identity_category"
16
+ )
17
+ .not("block_row", "is", null)
18
+ .limit(11000),
19
+ supabase
20
+ .from("profiles")
21
+ .select("wallet_address, x_username")
22
+ .not("x_username", "is", null),
23
+ ]);
24
+
25
+ if (walletsResult.error) {
26
+ throw new Error(`DB error: ${walletsResult.error.message}`);
27
+ }
28
+
29
+ const profileMap = new Map<string, string>();
30
+ for (const p of profilesResult.data ?? []) {
31
+ if (p.wallet_address && p.x_username) {
32
+ profileMap.set(p.wallet_address, p.x_username);
33
+ }
34
+ }
35
+
36
+ const wallets = (walletsResult.data ?? []).map((row) => ({
37
+ address: row.address,
38
+ txnCount: row.txn_count,
39
+ walletAgeDays: row.wallet_age_days,
40
+ volumeTraded: row.volume_traded_sol,
41
+ feesPaid: row.fees_paid_sol,
42
+ blockRow: row.block_row,
43
+ blockCol: row.block_col,
44
+ localSlot: row.local_slot,
45
+ uniqueTokensSwapped: row.unique_tokens_swapped,
46
+ latestBlocktime: row.latest_tx_at ? Math.floor(new Date(row.latest_tx_at).getTime() / 1000) : null,
47
+ identityName: row.identity_name ?? null,
48
+ identityType: row.identity_type ?? null,
49
+ identityCategory: row.identity_category ?? null,
50
+ xUsername: profileMap.get(row.address) ?? null,
51
+ }));
52
+
53
+ return NextResponse.json(
54
+ { wallets, totalPlaced: wallets.length },
55
+ {
56
+ headers: {
57
+ "Cache-Control": "public, max-age=30",
58
+ },
59
+ }
60
+ );
61
+ } catch (err) {
62
+ console.error("Wallets fetch error:", err);
63
+ return NextResponse.json(
64
+ { error: err instanceof Error ? err.message : "Unknown error" },
65
+ { status: 500 }
66
+ );
67
+ }
68
+ }
@@ -0,0 +1,76 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { createAdminClient } from "@/lib/supabase-admin";
3
+
4
+ /**
5
+ * Helius Enhanced Webhook handler.
6
+ * Receives SWAP events, stores them, and broadcasts to Supabase Realtime.
7
+ */
8
+ export async function POST(req: NextRequest) {
9
+ // Verify auth header
10
+ const authHeader = req.headers.get("authorization");
11
+ const expected = `Bearer ${process.env.HELIUS_WEBHOOK_SECRET}`;
12
+ if (authHeader !== expected) {
13
+ return NextResponse.json({ error: "unauthorized" }, { status: 401 });
14
+ }
15
+
16
+ try {
17
+ const events = await req.json();
18
+ if (!Array.isArray(events) || events.length === 0) {
19
+ return NextResponse.json({ received: 0 });
20
+ }
21
+
22
+ const supabase = createAdminClient();
23
+ let stored = 0;
24
+
25
+ for (const event of events) {
26
+ // Only process SWAP type
27
+ if (event.type !== "SWAP") continue;
28
+
29
+ const swap = event.events?.swap;
30
+ if (!swap) continue;
31
+
32
+ const feePayer = event.feePayer;
33
+ const signature = event.signature;
34
+ const timestamp = event.timestamp
35
+ ? new Date(event.timestamp * 1000).toISOString()
36
+ : new Date().toISOString();
37
+
38
+ // Extract token in/out from swap event
39
+ const tokenIn =
40
+ swap.tokenInputs?.[0]?.mint ??
41
+ (swap.nativeInput?.account ? "So11111111111111111111111111111111" : null);
42
+ const tokenOut =
43
+ swap.tokenOutputs?.[0]?.mint ??
44
+ (swap.nativeOutput?.account ? "So11111111111111111111111111111111" : null);
45
+
46
+ // Compute SOL amount
47
+ let amountSol: number | null = null;
48
+ if (swap.nativeInput?.amount) {
49
+ amountSol = Number(swap.nativeInput.amount) / 1e9;
50
+ } else if (swap.nativeOutput?.amount) {
51
+ amountSol = Number(swap.nativeOutput.amount) / 1e9;
52
+ }
53
+
54
+
55
+ // Insert swap event
56
+ const { error } = await supabase.from("swap_events").insert({
57
+ wallet_address: feePayer,
58
+ signature,
59
+ token_in: tokenIn,
60
+ token_out: tokenOut,
61
+ amount_sol: amountSol,
62
+ created_at: timestamp,
63
+ });
64
+
65
+ if (!error) stored++;
66
+ }
67
+
68
+ return NextResponse.json({ received: events.length, stored });
69
+ } catch (err) {
70
+ console.error("Webhook error:", err);
71
+ return NextResponse.json(
72
+ { error: String(err) },
73
+ { status: 500 }
74
+ );
75
+ }
76
+ }
@@ -0,0 +1,29 @@
1
+ "use client";
2
+ import { useEffect } from "react";
3
+ import { useRouter } from "next/navigation";
4
+ import { usePhantom } from "@phantom/react-sdk";
5
+
6
+ export default function AuthCallback() {
7
+ const router = useRouter();
8
+ const { isConnected } = usePhantom();
9
+
10
+ useEffect(() => {
11
+ if (isConnected) {
12
+ router.replace("/");
13
+ }
14
+ }, [isConnected, router]);
15
+
16
+ useEffect(() => {
17
+ const timer = setTimeout(() => router.replace("/"), 3000);
18
+ return () => clearTimeout(timer);
19
+ }, [router]);
20
+
21
+ return (
22
+ <div className="min-h-screen bg-[#0a0a12] flex items-center justify-center">
23
+ <div className="text-center">
24
+ <div className="w-8 h-8 border-2 border-white/20 border-t-white rounded-full animate-spin mx-auto mb-4" />
25
+ <p className="text-white/60 text-sm">Connecting wallet...</p>
26
+ </div>
27
+ </div>
28
+ );
29
+ }
Binary file