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,35 @@
1
+ /**
2
+ * City slot allocation constants.
3
+ *
4
+ * Slots are assigned in wallet-array order (ordered by placement in the DB).
5
+ * The first 100 wallets get a plane, the first 250 get a car, and the total
6
+ * city capacity is 1,000 wallets.
7
+ */
8
+
9
+ export const MAX_WALLETS = 1000;
10
+ export const MAX_PLANES = 100;
11
+ export const MAX_CARS = 250;
12
+
13
+ export interface SlotCounts {
14
+ totalWallets: number;
15
+ planesUsed: number;
16
+ carsUsed: number;
17
+ walletsRemaining: number;
18
+ planesRemaining: number;
19
+ carsRemaining: number;
20
+ }
21
+
22
+ /**
23
+ * Compute how many planes, cars, and wallet slots are claimed vs available.
24
+ */
25
+ export function getSlotCounts(walletCount: number): SlotCounts {
26
+ const clamped = Math.min(walletCount, MAX_WALLETS);
27
+ return {
28
+ totalWallets: clamped,
29
+ planesUsed: Math.min(clamped, MAX_PLANES),
30
+ carsUsed: Math.min(clamped, MAX_CARS),
31
+ walletsRemaining: MAX_WALLETS - clamped,
32
+ planesRemaining: MAX_PLANES - Math.min(clamped, MAX_PLANES),
33
+ carsRemaining: MAX_CARS - Math.min(clamped, MAX_CARS),
34
+ };
35
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * City zoning system — assigns block types based on distance from city center.
3
+ *
4
+ * Zone layout (concentric rings):
5
+ * DOWNTOWN — center ~20% of blocks: skyscrapers (height 15–45)
6
+ * MIDRISE — next ring ~30%: mid-level buildings (height 5–15)
7
+ * LOWRISE — outer ring ~40%: low buildings (height 0.5–5)
8
+ * PARK — 10 hardcoded park blocks
9
+ */
10
+
11
+ export type ZoneType = "downtown" | "midrise" | "lowrise" | "park";
12
+
13
+ export interface BlockZone {
14
+ zone: ZoneType;
15
+ maxHeight: number;
16
+ minHeight: number;
17
+ }
18
+
19
+ const ZONE_CONFIGS: Record<Exclude<ZoneType, "park">, { minHeight: number; maxHeight: number }> = {
20
+ downtown: { minHeight: 12, maxHeight: 45 },
21
+ midrise: { minHeight: 4, maxHeight: 14 },
22
+ lowrise: { minHeight: 0.5, maxHeight: 5 },
23
+ };
24
+
25
+ /**
26
+ * Given block grid coordinates and total grid size, determine what zone the block falls in.
27
+ */
28
+ export function getBlockZone(
29
+ blockRow: number,
30
+ blockCol: number,
31
+ blocksPerRow: number,
32
+ ): BlockZone {
33
+ const centerRow = (blocksPerRow - 1) / 2;
34
+ const centerCol = (blocksPerRow - 1) / 2;
35
+
36
+ const dr = blockRow - centerRow;
37
+ const dc = blockCol - centerCol;
38
+ const dist = Math.sqrt(dr * dr + dc * dc);
39
+
40
+ const maxDist = Math.sqrt(centerRow * centerRow + centerCol * centerCol) || 1;
41
+ const normDist = dist / maxDist;
42
+
43
+ if (isParkBlock(blockRow, blockCol)) {
44
+ return { zone: "park", minHeight: 0, maxHeight: 0 };
45
+ }
46
+
47
+ if (normDist <= 0.38) {
48
+ return { zone: "downtown", ...ZONE_CONFIGS.downtown };
49
+ }
50
+ if (normDist <= 0.76) {
51
+ return { zone: "midrise", ...ZONE_CONFIGS.midrise };
52
+ }
53
+ return { zone: "lowrise", ...ZONE_CONFIGS.lowrise };
54
+ }
55
+
56
+ /** Hardcoded park positions matching the DB block_spiral_order seed */
57
+ const PARK_BLOCKS = new Set([
58
+ "10,13", "11,13", "13,11", "12,3", "13,3",
59
+ "8,18", "20,12", "4,10", "18,21", "22,6",
60
+ ]);
61
+
62
+ export function isParkBlock(row: number, col: number): boolean {
63
+ return PARK_BLOCKS.has(`${row},${col}`);
64
+ }
@@ -0,0 +1,147 @@
1
+ import { PlacedWallet } from "@/types/wallet";
2
+ import {
3
+ getWalletWorldPosition,
4
+ getBuildingDimensions,
5
+ setMaxVolume,
6
+ } from "./building-math";
7
+ import { computeHouseSlots } from "@/components/InstancedHouses";
8
+ import {
9
+ CELL_SIZE,
10
+ BLOCK_SIZE,
11
+ BLOCK_STRIDE,
12
+ BLOCKS_PER_ROW,
13
+ OFFSET_X,
14
+ OFFSET_Z,
15
+ SLOTS_PER_BLOCK,
16
+ PARK_BLOCKS,
17
+ } from "./city-constants";
18
+
19
+ export interface AABB {
20
+ minX: number;
21
+ maxX: number;
22
+ minZ: number;
23
+ maxZ: number;
24
+ }
25
+
26
+ const BUCKET_SIZE = 4; // matches CELL_SIZE
27
+
28
+ function bucketKey(bx: number, bz: number): number {
29
+ // Pack two ints into one number (offset to avoid negatives)
30
+ return (bx + 500) * 10000 + (bz + 500);
31
+ }
32
+
33
+ export class CollisionMap {
34
+ private buckets = new Map<number, AABB[]>();
35
+
36
+ addAABB(aabb: AABB) {
37
+ const minBX = Math.floor(aabb.minX / BUCKET_SIZE);
38
+ const maxBX = Math.floor(aabb.maxX / BUCKET_SIZE);
39
+ const minBZ = Math.floor(aabb.minZ / BUCKET_SIZE);
40
+ const maxBZ = Math.floor(aabb.maxZ / BUCKET_SIZE);
41
+
42
+ for (let bx = minBX; bx <= maxBX; bx++) {
43
+ for (let bz = minBZ; bz <= maxBZ; bz++) {
44
+ const key = bucketKey(bx, bz);
45
+ let list = this.buckets.get(key);
46
+ if (!list) {
47
+ list = [];
48
+ this.buckets.set(key, list);
49
+ }
50
+ list.push(aabb);
51
+ }
52
+ }
53
+ }
54
+
55
+ testAABB(test: AABB): { hit: boolean; pushX: number; pushZ: number } {
56
+ const minBX = Math.floor(test.minX / BUCKET_SIZE);
57
+ const maxBX = Math.floor(test.maxX / BUCKET_SIZE);
58
+ const minBZ = Math.floor(test.minZ / BUCKET_SIZE);
59
+ const maxBZ = Math.floor(test.maxZ / BUCKET_SIZE);
60
+
61
+ let pushX = 0;
62
+ let pushZ = 0;
63
+ let hit = false;
64
+
65
+ // Collect unique AABBs from relevant buckets
66
+ const checked = new Set<AABB>();
67
+
68
+ for (let bx = minBX; bx <= maxBX; bx++) {
69
+ for (let bz = minBZ; bz <= maxBZ; bz++) {
70
+ const list = this.buckets.get(bucketKey(bx, bz));
71
+ if (!list) continue;
72
+ for (const aabb of list) {
73
+ if (checked.has(aabb)) continue;
74
+ checked.add(aabb);
75
+
76
+ // AABB overlap test
77
+ if (
78
+ test.maxX <= aabb.minX ||
79
+ test.minX >= aabb.maxX ||
80
+ test.maxZ <= aabb.minZ ||
81
+ test.minZ >= aabb.maxZ
82
+ ) continue;
83
+
84
+ hit = true;
85
+
86
+ // Minimum translation vector
87
+ const overlapLeft = test.maxX - aabb.minX;
88
+ const overlapRight = aabb.maxX - test.minX;
89
+ const overlapTop = test.maxZ - aabb.minZ;
90
+ const overlapBottom = aabb.maxZ - test.minZ;
91
+
92
+ const minOverlapX = overlapLeft < overlapRight ? -overlapLeft : overlapRight;
93
+ const minOverlapZ = overlapTop < overlapBottom ? -overlapTop : overlapBottom;
94
+
95
+ if (Math.abs(minOverlapX) < Math.abs(minOverlapZ)) {
96
+ pushX += minOverlapX;
97
+ } else {
98
+ pushZ += minOverlapZ;
99
+ }
100
+ }
101
+ }
102
+ }
103
+
104
+ return { hit, pushX, pushZ };
105
+ }
106
+ }
107
+
108
+ export function buildCollisionMap(wallets: PlacedWallet[]): CollisionMap {
109
+ const map = new CollisionMap();
110
+
111
+ // Ensure maxVolume is set for building dimension calculations
112
+ setMaxVolume(wallets);
113
+
114
+ // 1. Buildings
115
+ for (const w of wallets) {
116
+ const dims = getBuildingDimensions(w);
117
+ const pos = getWalletWorldPosition(w, dims);
118
+ const halfW = dims.width / 2;
119
+ const halfD = dims.depth / 2;
120
+ map.addAABB({
121
+ minX: pos[0] - halfW,
122
+ maxX: pos[0] + halfW,
123
+ minZ: pos[2] - halfD,
124
+ maxZ: pos[2] + halfD,
125
+ });
126
+ }
127
+
128
+ // 2. Houses
129
+ const occupiedSlots = new Set<string>();
130
+ for (const w of wallets) {
131
+ occupiedSlots.add(`${w.blockRow},${w.blockCol},${w.localSlot}`);
132
+ }
133
+
134
+ const houses = computeHouseSlots(occupiedSlots);
135
+ for (const h of houses) {
136
+ // Axis-aligned approximation of rotated footprint
137
+ const halfExtent = Math.max(h.w, h.d) / 2;
138
+ map.addAABB({
139
+ minX: h.x - halfExtent,
140
+ maxX: h.x + halfExtent,
141
+ minZ: h.z - halfExtent,
142
+ maxZ: h.z + halfExtent,
143
+ });
144
+ }
145
+
146
+ return map;
147
+ }
@@ -0,0 +1,252 @@
1
+ import * as THREE from "three";
2
+
3
+ // Full cycle duration in seconds (12-min loop: ~5min day, transitions, ~5min night)
4
+ export const CYCLE_DURATION = 720;
5
+
6
+ export interface DayNightState {
7
+ skyColor: THREE.Color; // horizon tint used for fog
8
+ fogColor: THREE.Color;
9
+ fogNear: number;
10
+ fogFar: number;
11
+ ambientColor: THREE.Color;
12
+ ambientIntensity: number;
13
+ sunColor: THREE.Color;
14
+ sunIntensity: number;
15
+ sunPosition: THREE.Vector3;
16
+ hemiSkyColor: THREE.Color;
17
+ hemiGroundColor: THREE.Color;
18
+ hemiIntensity: number;
19
+ accentColor: THREE.Color;
20
+ accentIntensity: number;
21
+ starsOpacity: number;
22
+ // Physically-based sky shader parameters (three <Sky> component)
23
+ turbidity: number; // 1–20 atmosphere density / haziness
24
+ rayleigh: number; // 0–4 Rayleigh scattering (blue sky strength)
25
+ mieCoefficient: number; // 0–0.1 Mie scattering (sun-disc haze radius)
26
+ mieDirectionalG: number; // 0–1 Mie directional (sun-disc sharpness)
27
+ }
28
+
29
+ interface Keyframe {
30
+ time: number;
31
+ sky: string;
32
+ fog: string;
33
+ fogNear: number;
34
+ fogFar: number;
35
+ ambCol: string;
36
+ ambInt: number;
37
+ sunCol: string;
38
+ sunInt: number;
39
+ hemiSky: string;
40
+ hemiGnd: string;
41
+ hemiInt: number;
42
+ accCol: string;
43
+ accInt: number;
44
+ stars: number;
45
+ turbidity: number;
46
+ rayleigh: number;
47
+ mie: number;
48
+ mieG: number;
49
+ }
50
+
51
+ // ─── Weather-grade photorealistic keyframes ───────────────────────────────────
52
+ // t=0.00 sunrise · t=0.08 morning · t=0.25 midday
53
+ // t=0.42 golden hour · t=0.50 sunset · t=0.58 blue hour
54
+ // t=0.75 midnight · t=0.92 pre-dawn
55
+ const KF: Keyframe[] = [
56
+ {
57
+ // ── SUNRISE — molten orange-rose horizon ──────────────────────────────────
58
+ time: 0.0,
59
+ sky: "#e8601a",
60
+ fog: "#b03c10",
61
+ fogNear: 260, fogFar: 700,
62
+ ambCol: "#ffb87a", ambInt: 0.32,
63
+ sunCol: "#ff7020", sunInt: 0.65,
64
+ hemiSky: "#ff9055", hemiGnd: "#5a2e14", hemiInt: 0.32,
65
+ accCol: "#7c3aed", accInt: 0.08,
66
+ stars: 0.12,
67
+ turbidity: 13, rayleigh: 0.7, mie: 0.022, mieG: 0.93,
68
+ },
69
+ {
70
+ // ── MORNING — champagne blue, long warm shadows ───────────────────────────
71
+ time: 0.08,
72
+ sky: "#68b8dc",
73
+ fog: "#90d0ea",
74
+ fogNear: 420, fogFar: 1100,
75
+ ambCol: "#fff5e0", ambInt: 0.54,
76
+ sunCol: "#ffe8b0", sunInt: 1.0,
77
+ hemiSky: "#a0dcf4", hemiGnd: "#7a5a36", hemiInt: 0.42,
78
+ accCol: "#6366f1", accInt: 0.0,
79
+ stars: 0.0,
80
+ turbidity: 4, rayleigh: 1.5, mie: 0.005, mieG: 0.88,
81
+ },
82
+ {
83
+ // ── MIDDAY — crisp cerulean, slight atmospheric haze ──────────────────────
84
+ time: 0.25,
85
+ sky: "#3a98cc",
86
+ fog: "#a8dcf2",
87
+ fogNear: 520, fogFar: 1400,
88
+ ambCol: "#ffffff", ambInt: 0.75,
89
+ sunCol: "#fffaf0", sunInt: 1.5,
90
+ hemiSky: "#78c8ec", hemiGnd: "#8a6a3a", hemiInt: 0.54,
91
+ accCol: "#6366f1", accInt: 0.0,
92
+ stars: 0.0,
93
+ turbidity: 2, rayleigh: 2.2, mie: 0.003, mieG: 0.8,
94
+ },
95
+ {
96
+ // ── GOLDEN HOUR — warm amber, sky turning from blue to gold ──────────────
97
+ time: 0.42,
98
+ sky: "#5898bc",
99
+ fog: "#80b8d8",
100
+ fogNear: 360, fogFar: 1050,
101
+ ambCol: "#ffeeaa", ambInt: 0.58,
102
+ sunCol: "#ffd060", sunInt: 1.0,
103
+ hemiSky: "#68a8cc", hemiGnd: "#684c1e", hemiInt: 0.44,
104
+ accCol: "#f97316", accInt: 0.06,
105
+ stars: 0.0,
106
+ turbidity: 6, rayleigh: 1.4, mie: 0.010, mieG: 0.91,
107
+ },
108
+ {
109
+ // ── SUNSET — blazing fire on the horizon, dark indigo above ───────────────
110
+ time: 0.50,
111
+ sky: "#c44010",
112
+ fog: "#9a3010",
113
+ fogNear: 320, fogFar: 880,
114
+ ambCol: "#ff9850", ambInt: 0.58,
115
+ sunCol: "#ff5010", sunInt: 0.80,
116
+ hemiSky: "#ff6a30", hemiGnd: "#7a4020", hemiInt: 0.46,
117
+ accCol: "#f97316", accInt: 0.30,
118
+ stars: 0.05,
119
+ turbidity: 16, rayleigh: 0.5, mie: 0.030, mieG: 0.95,
120
+ },
121
+ {
122
+ // ── BLUE HOUR — electric indigo, city starts glowing ─────────────────────
123
+ time: 0.58,
124
+ sky: "#0c1028",
125
+ fog: "#0c1028",
126
+ fogNear: 500, fogFar: 1200,
127
+ ambCol: "#8090cc", ambInt: 0.42,
128
+ sunCol: "#5058a8", sunInt: 0.10,
129
+ hemiSky: "#283068", hemiGnd: "#141414", hemiInt: 0.28,
130
+ accCol: "#6366f1", accInt: 1.4,
131
+ stars: 0.60,
132
+ turbidity: 6, rayleigh: 0.25, mie: 0.004, mieG: 0.7,
133
+ },
134
+ {
135
+ // ── MIDNIGHT — deep space, stars blazing, city fully lit ─────────────────
136
+ time: 0.75,
137
+ sky: "#03030c",
138
+ fog: "#04040e",
139
+ fogNear: 640, fogFar: 1600,
140
+ ambCol: "#b0b8f0", ambInt: 1.0, // keep high so buildings stay readable at night
141
+ sunCol: "#5868c0", sunInt: 0.30,
142
+ hemiSky: "#4858a0", hemiGnd: "#282838", hemiInt: 0.80,
143
+ accCol: "#6366f1", accInt: 2.6,
144
+ stars: 1.0,
145
+ turbidity: 1, rayleigh: 0.15, mie: 0.001, mieG: 0.5,
146
+ },
147
+ {
148
+ // ── PRE-DAWN — cold blue-black, stars fading ──────────────────────────────
149
+ time: 0.92,
150
+ sky: "#06091a",
151
+ fog: "#06091a",
152
+ fogNear: 560, fogFar: 1320,
153
+ ambCol: "#8888b8", ambInt: 0.48,
154
+ sunCol: "#3a3e80", sunInt: 0.07,
155
+ hemiSky: "#1e2248", hemiGnd: "#121212", hemiInt: 0.36,
156
+ accCol: "#6366f1", accInt: 1.5,
157
+ stars: 0.82,
158
+ turbidity: 2, rayleigh: 0.28, mie: 0.002, mieG: 0.65,
159
+ },
160
+ ];
161
+
162
+ // Pre-bake THREE.Color for every keyframe — no allocations during animation
163
+ const COLORS = KF.map((k) => ({
164
+ sky: new THREE.Color(k.sky),
165
+ fog: new THREE.Color(k.fog),
166
+ amb: new THREE.Color(k.ambCol),
167
+ sun: new THREE.Color(k.sunCol),
168
+ hemiSky: new THREE.Color(k.hemiSky),
169
+ hemiGnd: new THREE.Color(k.hemiGnd),
170
+ acc: new THREE.Color(k.accCol),
171
+ }));
172
+
173
+ // Reusable temp objects — zero per-frame allocation
174
+ const _sky = new THREE.Color();
175
+ const _fog = new THREE.Color();
176
+ const _amb = new THREE.Color();
177
+ const _sun = new THREE.Color();
178
+ const _hSky = new THREE.Color();
179
+ const _hGnd = new THREE.Color();
180
+ const _acc = new THREE.Color();
181
+ const _sunPos = new THREE.Vector3();
182
+
183
+ function smoothstep(t: number): number {
184
+ return t * t * (3 - 2 * t);
185
+ }
186
+
187
+ function lerp(a: number, b: number, t: number): number {
188
+ return a + (b - a) * t;
189
+ }
190
+
191
+ /** Lamp/headlight intensity — 0 in daylight, 1 at night, smooth transitions. */
192
+ export function lampIntensity(t: number): number {
193
+ if (t >= 0.58 && t <= 0.95) return 1;
194
+ if (t > 0.50 && t < 0.58) return (t - 0.50) / 0.08;
195
+ if (t > 0.95) return 1 - (t - 0.95) / 0.05;
196
+ if (t < 0.02) return 1 - t / 0.02;
197
+ return 0;
198
+ }
199
+
200
+ export function getDayNightState(timeOfDay: number): DayNightState {
201
+ const t = ((timeOfDay % 1) + 1) % 1;
202
+
203
+ let afterIdx = KF.findIndex((k) => k.time > t);
204
+ if (afterIdx === -1) afterIdx = 0;
205
+ const beforeIdx = (afterIdx - 1 + KF.length) % KF.length;
206
+
207
+ const before = KF[beforeIdx];
208
+ const after = KF[afterIdx];
209
+ const cB = COLORS[beforeIdx];
210
+ const cA = COLORS[afterIdx];
211
+
212
+ let progress: number;
213
+ if (after.time > before.time) {
214
+ progress = (t - before.time) / (after.time - before.time);
215
+ } else {
216
+ const segLen = 1 - before.time + after.time;
217
+ progress = t >= before.time
218
+ ? (t - before.time) / segLen
219
+ : (1 - before.time + t) / segLen;
220
+ }
221
+ progress = smoothstep(progress);
222
+
223
+ // Sun orbits the city: rises east (t=0), overhead (t=0.25), sets west (t=0.5), below (t=0.75)
224
+ const sunAngle = t * Math.PI * 2;
225
+ _sunPos.set(
226
+ Math.cos(sunAngle) * 200,
227
+ Math.sin(sunAngle) * 200, // allow full below-horizon travel for night
228
+ 30,
229
+ );
230
+
231
+ return {
232
+ skyColor: _sky.lerpColors(cB.sky, cA.sky, progress),
233
+ fogColor: _fog.lerpColors(cB.fog, cA.fog, progress),
234
+ fogNear: lerp(before.fogNear, after.fogNear, progress),
235
+ fogFar: lerp(before.fogFar, after.fogFar, progress),
236
+ ambientColor: _amb.lerpColors(cB.amb, cA.amb, progress),
237
+ ambientIntensity: lerp(before.ambInt, after.ambInt, progress),
238
+ sunColor: _sun.lerpColors(cB.sun, cA.sun, progress),
239
+ sunIntensity: lerp(before.sunInt, after.sunInt, progress),
240
+ sunPosition: _sunPos,
241
+ hemiSkyColor: _hSky.lerpColors(cB.hemiSky, cA.hemiSky, progress),
242
+ hemiGroundColor: _hGnd.lerpColors(cB.hemiGnd, cA.hemiGnd, progress),
243
+ hemiIntensity: lerp(before.hemiInt, after.hemiInt, progress),
244
+ accentColor: _acc.lerpColors(cB.acc, cA.acc, progress),
245
+ accentIntensity: lerp(before.accInt, after.accInt, progress),
246
+ starsOpacity: lerp(before.stars, after.stars, progress),
247
+ turbidity: lerp(before.turbidity, after.turbidity, progress),
248
+ rayleigh: lerp(before.rayleigh, after.rayleigh, progress),
249
+ mieCoefficient: lerp(before.mie, after.mie, progress),
250
+ mieDirectionalG: lerp(before.mieG, after.mieG, progress),
251
+ };
252
+ }
@@ -0,0 +1,28 @@
1
+ import { toPng } from "html-to-image";
2
+
3
+ export async function exportCardAsPng(element: HTMLElement): Promise<Blob> {
4
+ const dataUrl = await toPng(element, {
5
+ pixelRatio: 2,
6
+ width: 600,
7
+ height: 340,
8
+ });
9
+ const res = await fetch(dataUrl);
10
+ return res.blob();
11
+ }
12
+
13
+ export function downloadBlob(blob: Blob, filename: string) {
14
+ const url = URL.createObjectURL(blob);
15
+ const a = document.createElement("a");
16
+ a.href = url;
17
+ a.download = filename;
18
+ document.body.appendChild(a);
19
+ a.click();
20
+ document.body.removeChild(a);
21
+ URL.revokeObjectURL(url);
22
+ }
23
+
24
+ export async function copyBlobToClipboard(blob: Blob) {
25
+ await navigator.clipboard.write([
26
+ new ClipboardItem({ "image/png": blob }),
27
+ ]);
28
+ }
@@ -0,0 +1,90 @@
1
+ const HELIUS_WEBHOOKS_API = "https://api.helius.xyz/v0/webhooks";
2
+
3
+ function apiKey(): string {
4
+ return process.env.HELIUS_API_KEY!;
5
+ }
6
+
7
+ function webhookId(): string | undefined {
8
+ return process.env.HELIUS_WEBHOOK_ID || undefined;
9
+ }
10
+
11
+ function webhookSecret(): string | undefined {
12
+ return process.env.HELIUS_WEBHOOK_SECRET || undefined;
13
+ }
14
+
15
+ /** Create the swap webhook. Returns { webhookId, secret }. */
16
+ export async function createSwapWebhook(
17
+ webhookUrl: string,
18
+ addresses: string[],
19
+ ): Promise<{ webhookId: string; secret: string }> {
20
+ const secret = crypto.randomUUID();
21
+
22
+ const res = await fetch(`${HELIUS_WEBHOOKS_API}?api-key=${apiKey()}`, {
23
+ method: "POST",
24
+ headers: { "Content-Type": "application/json" },
25
+ body: JSON.stringify({
26
+ webhookURL: webhookUrl,
27
+ transactionTypes: ["SWAP"],
28
+ accountAddresses: addresses,
29
+ webhookType: "enhanced",
30
+ authHeader: `Bearer ${secret}`,
31
+ }),
32
+ });
33
+
34
+ if (!res.ok) {
35
+ const body = await res.text();
36
+ throw new Error(`Failed to create webhook: ${res.status} ${body}`);
37
+ }
38
+
39
+ const data = await res.json();
40
+ return { webhookId: data.webhookID, secret };
41
+ }
42
+
43
+ /**
44
+ * Add a single address to the existing swap webhook.
45
+ * Fetches the current webhook config, appends the address (if not already present),
46
+ * and PUTs the full object back (Helius requires the full object on update).
47
+ *
48
+ * No-ops silently if HELIUS_WEBHOOK_ID is not configured.
49
+ */
50
+ export async function addAddressToWebhook(address: string): Promise<void> {
51
+ const id = webhookId();
52
+ if (!id) return;
53
+
54
+ try {
55
+ // GET current webhook
56
+ const getRes = await fetch(
57
+ `${HELIUS_WEBHOOKS_API}/${id}?api-key=${apiKey()}`,
58
+ );
59
+ if (!getRes.ok) {
60
+ console.error(`Failed to get webhook: ${getRes.status}`);
61
+ return;
62
+ }
63
+ const webhook = await getRes.json();
64
+
65
+ // Skip if already tracked
66
+ const existing: string[] = webhook.accountAddresses ?? [];
67
+ if (existing.includes(address)) return;
68
+
69
+ // PUT with new address appended
70
+ existing.push(address);
71
+ const putRes = await fetch(
72
+ `${HELIUS_WEBHOOKS_API}/${id}?api-key=${apiKey()}`,
73
+ {
74
+ method: "PUT",
75
+ headers: { "Content-Type": "application/json" },
76
+ body: JSON.stringify({
77
+ ...webhook,
78
+ accountAddresses: existing,
79
+ }),
80
+ },
81
+ );
82
+
83
+ if (!putRes.ok) {
84
+ const body = await putRes.text();
85
+ console.error(`Failed to update webhook: ${putRes.status} ${body}`);
86
+ }
87
+ } catch (err) {
88
+ console.error("addAddressToWebhook error:", err);
89
+ }
90
+ }
@@ -0,0 +1,74 @@
1
+ const HELIUS_WALLET_API = "https://api.helius.xyz/v1/wallet";
2
+
3
+ export interface WalletIdentity {
4
+ name: string;
5
+ type: string;
6
+ category: string;
7
+ }
8
+
9
+ /**
10
+ * Call Helius Wallet API identity endpoint.
11
+ * Returns identity info for known wallets (exchanges, protocols, etc.) or null if unknown.
12
+ */
13
+ export async function getWalletIdentity(
14
+ address: string,
15
+ ): Promise<WalletIdentity | null> {
16
+ const url = `${HELIUS_WALLET_API}/${address}/identity?api-key=${process.env.HELIUS_API_KEY}`;
17
+ const res = await fetch(url);
18
+
19
+ if (res.status === 404) return null;
20
+ if (!res.ok) return null;
21
+
22
+ const data = await res.json();
23
+ if (!data.name) return null;
24
+
25
+ return {
26
+ name: data.name,
27
+ type: data.type ?? "unknown",
28
+ category: data.category ?? "",
29
+ };
30
+ }
31
+
32
+ export interface WalletFunding {
33
+ timestamp: number;
34
+ funder: string;
35
+ funderName: string | null;
36
+ amount: number;
37
+ }
38
+
39
+ export interface WalletStats {
40
+ address: string;
41
+ txnCount: number;
42
+ walletAgeDays: number;
43
+ volumeTraded: number;
44
+ feesPaid: number;
45
+ firstTxTimestamp: number | null;
46
+ ingestionStatus?: "queued" | "processing" | "complete" | "failed";
47
+ uniqueTokensSwapped?: number;
48
+ latestBlocktime?: number | null;
49
+ txnsFetched?: number;
50
+ }
51
+
52
+ /**
53
+ * Call Helius Wallet API funded-by endpoint.
54
+ * Returns funding info or null if 404 (e.g., funded via program/airdrop).
55
+ */
56
+ export async function getWalletFunding(
57
+ address: string,
58
+ ): Promise<WalletFunding | null> {
59
+ const url = `${HELIUS_WALLET_API}/${address}/funded-by?api-key=${process.env.HELIUS_API_KEY}`;
60
+ const res = await fetch(url);
61
+
62
+ if (res.status === 404) return null;
63
+ if (!res.ok) {
64
+ throw new Error(`Helius funded-by error: ${res.status} ${res.statusText}`);
65
+ }
66
+
67
+ const data = await res.json();
68
+ return {
69
+ timestamp: data.timestamp,
70
+ funder: data.funder,
71
+ funderName: data.funderName ?? null,
72
+ amount: data.amount,
73
+ };
74
+ }