dubs-server 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 (304) hide show
  1. package/.claude/settings.local.json +280 -0
  2. package/CLAUDE.md +46 -0
  3. package/CONNECT4_PRODUCTION_DEPLOY.md +155 -0
  4. package/CURRENT_SESSION.md +171 -0
  5. package/CURRENT_SESSION_DRAW.md +516 -0
  6. package/MARCH_MADNESS_SURVIVOR.md +254 -0
  7. package/PANDA.md +166 -0
  8. package/Procfile +4 -0
  9. package/README.md +476 -0
  10. package/controllers/livescoresController.js +376 -0
  11. package/controllers/pickemController.js +554 -0
  12. package/controllers/survivorAdminController.js +887 -0
  13. package/controllers/survivorController.js +623 -0
  14. package/cron/oracleMonitor.js +77 -0
  15. package/cron/pickemOracleMonitor.js +73 -0
  16. package/data/jackpot-history.json +952 -0
  17. package/data/ncaaTeams.js +406 -0
  18. package/documentation/API_SECURITY_GUIDE.md +327 -0
  19. package/documentation/ARCADE_API.md +593 -0
  20. package/documentation/ARCADE_IMPLEMENTATION_SUMMARY.md +399 -0
  21. package/documentation/ARCADE_QUICKSTART.md +242 -0
  22. package/documentation/AUTOMATIC_MODE_ORACLE.md +321 -0
  23. package/documentation/BUG_FIX_COHORT_DATE_DISPLAY.md +171 -0
  24. package/documentation/CLAIM_MIGRATION_INSTRUCTIONS.md +52 -0
  25. package/documentation/CLAIM_STATUS_FIX.md +67 -0
  26. package/documentation/CLI_TOOL_GUIDE.md +372 -0
  27. package/documentation/COHORT_RETENTION_ANALYSIS.md +295 -0
  28. package/documentation/COHORT_RETENTION_IMPLEMENTATION_COMPLETE.md +461 -0
  29. package/documentation/COHORT_RETENTION_SUMMARY.md +204 -0
  30. package/documentation/COMPLETE_PROJECT_SUMMARY.md +490 -0
  31. package/documentation/DATABASE_QUERIES.md +269 -0
  32. package/documentation/DATABASE_RETENTION_POLICY.md +390 -0
  33. package/documentation/DATABASE_SETUP_GUIDE.md +361 -0
  34. package/documentation/DATABASE_SETUP_SUMMARY.md +247 -0
  35. package/documentation/DEMO_API_CURL_COMMANDS.md +656 -0
  36. package/documentation/DEPLOYMENT_SUMMARY.txt +100 -0
  37. package/documentation/DUPLICATE_NOTIFICATIONS_FIXED.md +201 -0
  38. package/documentation/EXCHANGE_RATES_INTEGRATION.md +371 -0
  39. package/documentation/FINAL_API_PROTECTION_TABLE.md +175 -0
  40. package/documentation/GAME_START_NOTIFICATIONS_DEPLOYMENT.md +256 -0
  41. package/documentation/GAME_START_NOTIFICATIONS_INTEGRATION.md +275 -0
  42. package/documentation/HEROKU_DEPLOYMENT.md +134 -0
  43. package/documentation/HEROKU_SCHEDULER_SETUP.md +271 -0
  44. package/documentation/JACKPOT_API.md +521 -0
  45. package/documentation/JACKPOT_DEPLOYMENT_GUIDE.md +362 -0
  46. package/documentation/JWT_IMPLEMENTATION_SUMMARY.md +373 -0
  47. package/documentation/JWT_QUICK_SETUP.md +268 -0
  48. package/documentation/JWT_TESTING_GUIDE.md +404 -0
  49. package/documentation/KEEPER_RECOVERY_GUIDE.md +381 -0
  50. package/documentation/KEEPER_SETUP.md +206 -0
  51. package/documentation/KEEPER_STATE_MACHINE.md +423 -0
  52. package/documentation/LATEST_PRODUCTION_SETUP.md +387 -0
  53. package/documentation/LOCAL_VOTING_TEST.md +279 -0
  54. package/documentation/ORACLE_FIXES_SUMMARY.md +188 -0
  55. package/documentation/ORACLE_POSTGRESQL_UPDATE.md +202 -0
  56. package/documentation/PAYMENT_DEPLOYMENT.md +209 -0
  57. package/documentation/PNL_TRACKING_SETUP.md +189 -0
  58. package/documentation/PREVENTING_LOCKUP_ERRORS.md +472 -0
  59. package/documentation/PRODUCTION_READY_SUMMARY.md +227 -0
  60. package/documentation/PUBLIC_VS_PRIVATE_ENDPOINTS.md +278 -0
  61. package/documentation/QUICK_AUTH_SETUP.md +99 -0
  62. package/documentation/QUICK_DEPLOY.md +224 -0
  63. package/documentation/QUICK_FIX.md +114 -0
  64. package/documentation/QUICK_START.md +152 -0
  65. package/documentation/REFEREE_MODE_GUIDE.md +392 -0
  66. package/documentation/RETENTION_CORE_ACTION_UPDATE.md +313 -0
  67. package/documentation/RETENTION_UPDATE_SUMMARY.md +108 -0
  68. package/documentation/RUN_MIGRATION_NOW.md +39 -0
  69. package/documentation/SCRIPTS_UPDATE_SUMMARY.md +251 -0
  70. package/documentation/SETUP_GUIDE.md +184 -0
  71. package/documentation/STATE_MACHINE_IMPLEMENTATION.md +250 -0
  72. package/documentation/TELEGRAM_NOTIFICATIONS_DIAGNOSIS.md +361 -0
  73. package/documentation/UNIFIED_ARCHITECTURE.md +231 -0
  74. package/documentation/VOTING_DEPLOYMENT_SUMMARY.md +392 -0
  75. package/documentation/WEBSOCKET_ARCHITECTURE.md +881 -0
  76. package/documentation/WHAT_WE_BUILT_TODAY.md +369 -0
  77. package/documentation/latest/LATEST_PRODUCTION_SETUP.md +865 -0
  78. package/ecosystem.config.js +65 -0
  79. package/env.template +125 -0
  80. package/middleware/apiKeyAuth.js +136 -0
  81. package/middleware/authenticate.js +214 -0
  82. package/middleware/developerUserAuth.js +76 -0
  83. package/middleware/socketAuth.js +69 -0
  84. package/package.json +49 -0
  85. package/postman/Dubs-API-v1-With-Voting.postman_collection.json +555 -0
  86. package/postman/Dubs-API-v1.postman_collection.json +205 -0
  87. package/postman/Dubs_Developer_API.postman_collection.json +662 -0
  88. package/postman/QUICKSTART.md +118 -0
  89. package/postman/QUICK_REFERENCE.md +246 -0
  90. package/postman/README.md +71 -0
  91. package/postman/VOTING_API_GUIDE.md +426 -0
  92. package/refactor/Animations.md +148 -0
  93. package/refactor/Chat.md +252 -0
  94. package/routes/actionsRoutes.js +699 -0
  95. package/routes/adminRoutes.js +370 -0
  96. package/routes/analyticsRoutes.js +1262 -0
  97. package/routes/arcadeRoutes.js +557 -0
  98. package/routes/authRoutes.js +2310 -0
  99. package/routes/avatarRoutes.js +85 -0
  100. package/routes/botRoutes.js +211 -0
  101. package/routes/chatRoutes.js +377 -0
  102. package/routes/cryptoPriceRoutes.js +105 -0
  103. package/routes/developerRoutes.js +4201 -0
  104. package/routes/deviceRoutes.js +214 -0
  105. package/routes/dmRoutes.js +167 -0
  106. package/routes/esportsRoutes.js +806 -0
  107. package/routes/exchangeRateRoutes.js +233 -0
  108. package/routes/gamesRoutes.js +3028 -0
  109. package/routes/jackpotRoutes.js +754 -0
  110. package/routes/keeperMonitoringRoutes.js +156 -0
  111. package/routes/keeperWebhookRoutes.js +466 -0
  112. package/routes/livescoresRoutes.js +31 -0
  113. package/routes/pickemAdminRoutes.js +199 -0
  114. package/routes/pickemRoutes.js +231 -0
  115. package/routes/playerStatsRoutes.js +147 -0
  116. package/routes/portfolioRoutes.js +217 -0
  117. package/routes/promoRoutes.js +418 -0
  118. package/routes/referralEarningsRoutes.js +392 -0
  119. package/routes/socialRoutes.js +459 -0
  120. package/routes/sportsRoutes.js +1271 -0
  121. package/routes/survivorAdminRoutes.js +345 -0
  122. package/routes/survivorRoutes.js +756 -0
  123. package/routes/uploadRoutes.js +256 -0
  124. package/routes/userProfileRoutes.js +244 -0
  125. package/routes/whatsNewRoutes.js +331 -0
  126. package/scripts/.claude/settings.local.json +15 -0
  127. package/scripts/README.md +170 -0
  128. package/scripts/RESTART_EVERYTHING.sh +104 -0
  129. package/scripts/add-claim-columns.sql +48 -0
  130. package/scripts/add-crypto-prices-cache.sql +27 -0
  131. package/scripts/add-exchange-rates-cache.sql +40 -0
  132. package/scripts/add-game-invite-column.sql +23 -0
  133. package/scripts/add-game-invite-notification.sql +33 -0
  134. package/scripts/add-game-invite-telegram-pref.sql +16 -0
  135. package/scripts/add-game-joined-notification.sql +16 -0
  136. package/scripts/add-game-joined-pref.js +40 -0
  137. package/scripts/add-game-joined-preference.sql +6 -0
  138. package/scripts/add-game-start-notifications.sql +41 -0
  139. package/scripts/add-notification-flags-to-games.sql +55 -0
  140. package/scripts/add-pending-game-dismissals.sql +19 -0
  141. package/scripts/add-preferred-currency.sql +34 -0
  142. package/scripts/add-winner-columns.js +61 -0
  143. package/scripts/add_mention_system.sql +53 -0
  144. package/scripts/add_payment_system.sql +96 -0
  145. package/scripts/add_sports_event_id_column.sql +22 -0
  146. package/scripts/analyze-cohort-data-heroku.js +276 -0
  147. package/scripts/analyze-cohort-data.js +295 -0
  148. package/scripts/analyze-prod-cohorts.sh +10 -0
  149. package/scripts/backfill-matchup-images.js +245 -0
  150. package/scripts/backfill-missing-signatures.js +175 -0
  151. package/scripts/backfill-referral-earnings.js +202 -0
  152. package/scripts/check-chat-schema.js +130 -0
  153. package/scripts/check-db.sh +14 -0
  154. package/scripts/check_oracle_in_game.js +54 -0
  155. package/scripts/cleanup-database.js +193 -0
  156. package/scripts/clear-notification-cache.js +85 -0
  157. package/scripts/convert-mnemonic.js +50 -0
  158. package/scripts/create-users-table.sql +44 -0
  159. package/scripts/debug-cohort-counts.js +248 -0
  160. package/scripts/debug-winner-calc.js +84 -0
  161. package/scripts/deploy-payment-system.sh +118 -0
  162. package/scripts/deploy-to-heroku.sh +63 -0
  163. package/scripts/diagnose-locked-round.js +143 -0
  164. package/scripts/dubs-cli.js +720 -0
  165. package/scripts/dump-account.js +65 -0
  166. package/scripts/find-vrf-offset.js +48 -0
  167. package/scripts/fix-chat-notifications-constraint.sql +122 -0
  168. package/scripts/fix-claim-columns.js +124 -0
  169. package/scripts/fix-constraint-now.js +44 -0
  170. package/scripts/fix-lock-timestamps.js +96 -0
  171. package/scripts/fix-locked-round.sh +126 -0
  172. package/scripts/fix-missing-badges.sql +91 -0
  173. package/scripts/fix-payment-notifications.sql +41 -0
  174. package/scripts/force-new-round.js +55 -0
  175. package/scripts/force-resolve-and-claim.js +278 -0
  176. package/scripts/important/README.md +115 -0
  177. package/scripts/important/authority-force-lock.js +197 -0
  178. package/scripts/important/authority-resolve-game.js +267 -0
  179. package/scripts/important/check-game-status.js +373 -0
  180. package/scripts/important/list-pending-games-by-version.js +270 -0
  181. package/scripts/important/reconcile-v1-v2-payouts.js +270 -0
  182. package/scripts/initialize-jackpot.js +111 -0
  183. package/scripts/jackpot/.claude/settings.local.json +10 -0
  184. package/scripts/jackpot/force-reset.js +84 -0
  185. package/scripts/jackpot/initialize-mainnet.js +100 -0
  186. package/scripts/jackpot/keeper.js +742 -0
  187. package/scripts/jackpot/status.js +107 -0
  188. package/scripts/jackpot/update-round-duration.js +143 -0
  189. package/scripts/keeper-bot.js +112 -0
  190. package/scripts/list-pending-games.js +131 -0
  191. package/scripts/migrate-chat-v2.js +127 -0
  192. package/scripts/migrate-chat-winners.js +84 -0
  193. package/scripts/migrate-chat.sh +17 -0
  194. package/scripts/migrate-game-invite.js +83 -0
  195. package/scripts/migrate-heroku-game-notifications.sh +159 -0
  196. package/scripts/migrations/001_analytics_tables.sql +422 -0
  197. package/scripts/migrations/002_add_matchup_image_url.sql +14 -0
  198. package/scripts/migrations/003_referral_earnings.sql +208 -0
  199. package/scripts/migrations/004_add_whats_new_notification_type.sql +62 -0
  200. package/scripts/migrations/005_add_connect4_your_turn_notification.sql +61 -0
  201. package/scripts/migrations/005_push_notifications.sql +55 -0
  202. package/scripts/migrations/006_add_draw_team_players.sql +28 -0
  203. package/scripts/migrations/006_add_game_cancelled_notification.sql +62 -0
  204. package/scripts/migrations/007_add_gif_url.sql +8 -0
  205. package/scripts/migrations/008_add_connect4_columns.sql +139 -0
  206. package/scripts/migrations/008_add_pool_tracking.sql +22 -0
  207. package/scripts/migrations/009_create_survivor_pool_tables.sql +174 -0
  208. package/scripts/migrations/010_add_survivor_pool_outcome.sql +28 -0
  209. package/scripts/migrations/011_create_developer_tables.sql +67 -0
  210. package/scripts/migrations/011_fix_keeper_tables.sql +85 -0
  211. package/scripts/migrations/012_create_developer_webhooks.sql +31 -0
  212. package/scripts/migrations/013_add_network_mode.sql +18 -0
  213. package/scripts/migrations/014_create_developer_app_users.sql +19 -0
  214. package/scripts/migrations/015_add_ui_config.sql +4 -0
  215. package/scripts/migrations/016_add_resolution_secret.sql +4 -0
  216. package/scripts/migrations/017_add_external_game_id.sql +3 -0
  217. package/scripts/migrations/018_create_pickem_tables.sql +115 -0
  218. package/scripts/migrations/019_expo_push_tokens.sql +19 -0
  219. package/scripts/migrations/create_whats_new_tables.sql +88 -0
  220. package/scripts/migrations/drop_live_games_tables.sql +34 -0
  221. package/scripts/open-jackpot-round.js +85 -0
  222. package/scripts/purge-all-data.sh +329 -0
  223. package/scripts/purge-all-data.sql +142 -0
  224. package/scripts/purge-heroku-data.sh +149 -0
  225. package/scripts/purge-heroku-data.sql +62 -0
  226. package/scripts/rebuild-heroku-database.sh +113 -0
  227. package/scripts/recover-funds.js +357 -0
  228. package/scripts/regenerate-epl-images.js +278 -0
  229. package/scripts/resize-s3-matchup-images.js +374 -0
  230. package/scripts/resolve-direct.js +88 -0
  231. package/scripts/resolve-mock-game.js +124 -0
  232. package/scripts/resolve-pickem-game.js +55 -0
  233. package/scripts/resolve-round-manual.js +83 -0
  234. package/scripts/resolve-stuck-game.js +382 -0
  235. package/scripts/resolve-stuck-round.js +42 -0
  236. package/scripts/run-connect4-migration.sh +16 -0
  237. package/scripts/run-mention-migration.sh +32 -0
  238. package/scripts/run-payment-migration.sh +51 -0
  239. package/scripts/run-preferred-currency-migration.sh +31 -0
  240. package/scripts/run-referral-earnings-migration.sh +32 -0
  241. package/scripts/run-survivor-outcome-migration.sh +16 -0
  242. package/scripts/seed-test-users.js +346 -0
  243. package/scripts/setup-auth-tables.js +78 -0
  244. package/scripts/setup-complete-database.sql +992 -0
  245. package/scripts/setup-database-fresh.sh +359 -0
  246. package/scripts/setup-heroku-keeper.sh +48 -0
  247. package/scripts/setup-keeper-database.js +83 -0
  248. package/scripts/setup-keeper-state-db.sql +110 -0
  249. package/scripts/setup-oracle.sh +39 -0
  250. package/scripts/setup-pnl-tracking.js +111 -0
  251. package/scripts/start-devnet.sh +14 -0
  252. package/scripts/test-arcade-devnet.sh +160 -0
  253. package/scripts/test-arcade-match.sh +109 -0
  254. package/scripts/test-automatic-mode.sh +239 -0
  255. package/scripts/test-connect4-cancel-claim.js +370 -0
  256. package/scripts/test-connect4-e2e.js +369 -0
  257. package/scripts/test-connect4-resolve.js +369 -0
  258. package/scripts/test-game-state-endpoint.js +136 -0
  259. package/scripts/test-invite-notification.js +86 -0
  260. package/scripts/test-jackpot-api.sh +71 -0
  261. package/scripts/test-poll-confirmation.js +267 -0
  262. package/scripts/test-resolve-game.js +271 -0
  263. package/scripts/test-resolve-signature.js +223 -0
  264. package/scripts/test-signature-preservation.js +124 -0
  265. package/scripts/test-state-machine.js +291 -0
  266. package/scripts/test-webhook-receiver.js +60 -0
  267. package/scripts/update-notification-constraint.js +52 -0
  268. package/scripts/verify-account-layout.js +145 -0
  269. package/scripts/verify-winner-algorithm.js +278 -0
  270. package/server.js +5259 -0
  271. package/services/arcadeMatchService.js +763 -0
  272. package/services/automaticGameOracle.js +1596 -0
  273. package/services/chatService.js +1612 -0
  274. package/services/connect4GameService.js +1049 -0
  275. package/services/connect4NotificationService.js +374 -0
  276. package/services/cryptoPriceService.js +223 -0
  277. package/services/customGameResolver.js +260 -0
  278. package/services/db.js +79 -0
  279. package/services/directMessageService.js +389 -0
  280. package/services/discordNotifications.js +160 -0
  281. package/services/exchangeRateService.js +289 -0
  282. package/services/expoPushService.js +314 -0
  283. package/services/gamesCacheService.js +539 -0
  284. package/services/jackpotHistory.js +331 -0
  285. package/services/jackpotService.js +856 -0
  286. package/services/keeperStateService.js +355 -0
  287. package/services/matchupImageService.js +591 -0
  288. package/services/notificationCacheService.js +407 -0
  289. package/services/pickemOracle.js +440 -0
  290. package/services/playerStatsService.js +389 -0
  291. package/services/portfolioService.js +555 -0
  292. package/services/promoService.js +757 -0
  293. package/services/promoTreasuryService.js +239 -0
  294. package/services/pushNotifications.js +353 -0
  295. package/services/redisService.js +422 -0
  296. package/services/referralEarningsService.js +728 -0
  297. package/services/s3Service.js +396 -0
  298. package/services/socialService.js +1202 -0
  299. package/services/survivorOracle.js +469 -0
  300. package/services/survivorSimulator.js +475 -0
  301. package/services/telegramNotifications.js +461 -0
  302. package/services/userProfileStatsService.js +1185 -0
  303. package/services/whatsNewService.js +388 -0
  304. package/utils/urlHelper.js +95 -0
@@ -0,0 +1,754 @@
1
+ /**
2
+ * 🎰 Jackpot API Routes
3
+ *
4
+ * RESTful API for Solpot-style continuous jackpot
5
+ * Isolated from sports betting and arcade systems
6
+ */
7
+
8
+ const express = require('express');
9
+ const router = express.Router();
10
+ const crypto = require('crypto');
11
+ const { instance: history } = require('../services/jackpotHistory');
12
+ const promoService = require('../services/promoService');
13
+ const promoTreasuryService = require('../services/promoTreasuryService');
14
+
15
+ module.exports = (jackpotService) => {
16
+
17
+ // ============================================
18
+ // 🏗️ BUILD TRANSACTION ENDPOINTS
19
+ // Returns unsigned transactions for frontend to sign
20
+ // ============================================
21
+
22
+ /**
23
+ * POST /jackpot/build/initialize
24
+ * Build UNSIGNED transaction for initializing the protocol
25
+ *
26
+ * Body:
27
+ * {
28
+ * adminAddress: string (Solana pubkey),
29
+ * treasuryAddress: string (Solana pubkey),
30
+ * feeBasisPoints: number (e.g., 500 = 5%),
31
+ * roundDurationSlots: number (e.g., 1800 = ~12 min)
32
+ * }
33
+ */
34
+ router.post('/build/initialize', async (req, res) => {
35
+ try {
36
+ const {
37
+ adminAddress,
38
+ treasuryAddress,
39
+ feeBasisPoints = 500,
40
+ roundDurationSlots = 1800,
41
+ } = req.body;
42
+
43
+ if (!adminAddress || !treasuryAddress) {
44
+ return res.status(400).json({
45
+ error: 'Missing required fields: adminAddress, treasuryAddress'
46
+ });
47
+ }
48
+
49
+ if (feeBasisPoints > 1000) {
50
+ return res.status(400).json({
51
+ error: 'Fee cannot exceed 10% (1000 basis points)'
52
+ });
53
+ }
54
+
55
+ const result = await jackpotService.buildInitializeTransaction({
56
+ adminAddress,
57
+ treasuryAddress,
58
+ feeBasisPoints,
59
+ roundDurationSlots,
60
+ });
61
+
62
+ res.json(result);
63
+ } catch (error) {
64
+ console.error('Build initialize error:', error);
65
+ res.status(500).json({
66
+ error: error.message || 'Failed to build transaction',
67
+ });
68
+ }
69
+ });
70
+
71
+ /**
72
+ * POST /jackpot/build/update-config
73
+ * Build UNSIGNED transaction for updating protocol config (authority only)
74
+ *
75
+ * Body:
76
+ * {
77
+ * authorityAddress: string (Solana pubkey),
78
+ * feeBasisPoints?: number (e.g., 500 = 5%),
79
+ * roundDurationSlots?: number (e.g., 1512000 = ~1 week),
80
+ * newAuthority?: string (Solana pubkey)
81
+ * }
82
+ */
83
+ router.post('/build/update-config', async (req, res) => {
84
+ try {
85
+ const { authorityAddress, feeBasisPoints, roundDurationSlots, newAuthority } = req.body;
86
+
87
+ if (!authorityAddress) {
88
+ return res.status(400).json({
89
+ error: 'Missing required field: authorityAddress'
90
+ });
91
+ }
92
+
93
+ if (feeBasisPoints != null && feeBasisPoints > 1000) {
94
+ return res.status(400).json({
95
+ error: 'Fee cannot exceed 10% (1000 basis points)'
96
+ });
97
+ }
98
+
99
+ const result = await jackpotService.buildUpdateConfigTransaction({
100
+ authorityAddress,
101
+ feeBasisPoints,
102
+ roundDurationSlots,
103
+ newAuthority,
104
+ });
105
+
106
+ res.json(result);
107
+ } catch (error) {
108
+ console.error('Build update-config error:', error);
109
+ res.status(500).json({
110
+ error: error.message || 'Failed to build transaction',
111
+ });
112
+ }
113
+ });
114
+
115
+ /**
116
+ * POST /jackpot/build/open-round
117
+ * Build UNSIGNED transaction for opening a new round
118
+ *
119
+ * Body:
120
+ * {
121
+ * keeperAddress: string (Solana pubkey)
122
+ * }
123
+ */
124
+ router.post('/build/open-round', async (req, res) => {
125
+ try {
126
+ const { keeperAddress } = req.body;
127
+
128
+ if (!keeperAddress) {
129
+ return res.status(400).json({
130
+ error: 'Missing required field: keeperAddress'
131
+ });
132
+ }
133
+
134
+ const result = await jackpotService.buildOpenRoundTransaction({
135
+ keeperAddress,
136
+ });
137
+
138
+ res.json(result);
139
+ } catch (error) {
140
+ console.error('Build open round error:', error);
141
+ res.status(500).json({
142
+ error: error.message || 'Failed to build transaction',
143
+ });
144
+ }
145
+ });
146
+
147
+ /**
148
+ * POST /jackpot/build/enter
149
+ * Build UNSIGNED transaction for entering the current round
150
+ *
151
+ * Body:
152
+ * {
153
+ * playerAddress: string (Solana pubkey),
154
+ * amount: number (lamports),
155
+ * roundId?: string (optional, uses current round if not provided)
156
+ * }
157
+ */
158
+ router.post('/build/enter', async (req, res) => {
159
+ try {
160
+ const { playerAddress, amount, roundId } = req.body;
161
+
162
+ if (!playerAddress || !amount) {
163
+ return res.status(400).json({
164
+ error: 'Missing required fields: playerAddress, amount'
165
+ });
166
+ }
167
+
168
+ if (amount < 10000) {
169
+ return res.status(400).json({
170
+ error: 'Minimum entry amount is 10,000 lamports (0.00001 SOL)'
171
+ });
172
+ }
173
+
174
+ const result = await jackpotService.buildEnterRoundTransaction({
175
+ playerAddress,
176
+ amount,
177
+ roundId,
178
+ });
179
+
180
+ res.json(result);
181
+ } catch (error) {
182
+ console.error('Build enter round error:', error);
183
+ res.status(500).json({
184
+ error: error.message || 'Failed to build transaction',
185
+ });
186
+ }
187
+ });
188
+
189
+ /**
190
+ * POST /jackpot/build/enter-sponsored
191
+ * Build UNSIGNED transaction for entering via promo code (treasury pays)
192
+ * Compound tx: SystemProgram.transfer(treasury → player) + enter_round(player, amount)
193
+ *
194
+ * Body:
195
+ * {
196
+ * playerAddress: string (Solana pubkey),
197
+ * promoCode: string (e.g. "DUBS-XXXX-XXXX")
198
+ * }
199
+ */
200
+ router.post('/build/enter-sponsored', async (req, res) => {
201
+ try {
202
+ const { playerAddress, promoCode } = req.body;
203
+
204
+ if (!playerAddress || !promoCode) {
205
+ return res.status(400).json({
206
+ error: 'Missing required fields: playerAddress, promoCode'
207
+ });
208
+ }
209
+
210
+ // Verify treasury is ready
211
+ if (!promoTreasuryService.isReady()) {
212
+ return res.status(503).json({ error: 'Promo system is currently unavailable' });
213
+ }
214
+
215
+ // Get user's active promo (must be reserved first)
216
+ const activePromo = await promoService.getActivePromoForUser(playerAddress);
217
+ if (!activePromo) {
218
+ return res.status(400).json({ error: 'No active promo code found. Please reserve a code first.' });
219
+ }
220
+
221
+ // Verify the promo code matches
222
+ if (activePromo.code !== promoCode.trim().toUpperCase()) {
223
+ return res.status(400).json({ error: 'Promo code does not match your reserved code' });
224
+ }
225
+
226
+ // Check treasury balance
227
+ const balanceCheck = await promoTreasuryService.hasEnoughBalance(activePromo.amountLamports);
228
+ if (!balanceCheck.enough) {
229
+ return res.status(503).json({ error: 'Promo system temporarily unavailable' });
230
+ }
231
+
232
+ const treasuryKeypair = promoTreasuryService.getKeypair();
233
+
234
+ const result = await jackpotService.buildEnterRoundSponsoredTransaction({
235
+ playerAddress,
236
+ amount: activePromo.amountLamports,
237
+ treasuryKeypair,
238
+ });
239
+
240
+ // NOTE: Do NOT mark promo as used here!
241
+ // Frontend calls /api/promo/confirm-usage AFTER transaction is confirmed on-chain
242
+
243
+ res.json({
244
+ ...result,
245
+ promoCode: activePromo.code,
246
+ sponsorWallet: treasuryKeypair.publicKey.toString(),
247
+ });
248
+ } catch (error) {
249
+ console.error('Build enter-sponsored error:', error);
250
+ res.status(500).json({
251
+ error: error.message || 'Failed to build sponsored transaction',
252
+ });
253
+ }
254
+ });
255
+
256
+ /**
257
+ * POST /jackpot/player-joined
258
+ * Notify server that a player's transaction confirmed (triggers WebSocket broadcast)
259
+ *
260
+ * Body:
261
+ * {
262
+ * playerAddress: string,
263
+ * roundId: string,
264
+ * amount: number (lamports),
265
+ * signature: string
266
+ * }
267
+ */
268
+ router.post('/player-joined', async (req, res) => {
269
+ try {
270
+ const { playerAddress, roundId, amount, signature } = req.body;
271
+
272
+ console.log(`👤 Player joined notification: ${playerAddress.slice(0,8)}... for round ${roundId}`);
273
+
274
+ // Broadcast to all clients (globally + room) so every viewer gets the update
275
+ if (global.io) {
276
+ const event = {
277
+ roundId,
278
+ player: playerAddress,
279
+ weight: amount,
280
+ timestamp: Date.now(),
281
+ signature
282
+ };
283
+ global.io.to(`round_${roundId}`).emit('player_joined', event);
284
+ global.io.emit('player_joined', event);
285
+ console.log(`⚡ Broadcasted: player_joined globally to ALL clients`);
286
+ }
287
+
288
+ // Persist entry for transaction history linking
289
+ if (signature && playerAddress && roundId) {
290
+ history.addEntry({
291
+ walletAddress: playerAddress,
292
+ roundId: roundId.toString(),
293
+ amount: amount || 0,
294
+ signature,
295
+ }).catch(err => console.error('Failed to persist jackpot entry:', err));
296
+ }
297
+
298
+ res.json({ success: true, broadcasted: true });
299
+ } catch (error) {
300
+ console.error('Player joined notification error:', error);
301
+ res.status(500).json({ error: error.message });
302
+ }
303
+ });
304
+
305
+ /**
306
+ * POST /jackpot/build/lock-round
307
+ * Build UNSIGNED transaction for locking a round (commit phase)
308
+ *
309
+ * Body:
310
+ * {
311
+ * keeperAddress: string (Solana pubkey),
312
+ * serverSeed?: string (optional, auto-generated if not provided),
313
+ * roundId?: string (optional, uses current round if not provided)
314
+ * }
315
+ */
316
+ router.post('/build/lock-round', async (req, res) => {
317
+ try {
318
+ const { keeperAddress, roundId } = req.body;
319
+
320
+ if (!keeperAddress) {
321
+ return res.status(400).json({
322
+ error: 'Missing required field: keeperAddress'
323
+ });
324
+ }
325
+
326
+ const result = await jackpotService.buildLockRoundTransaction({
327
+ keeperAddress,
328
+ roundId,
329
+ });
330
+
331
+ res.json(result);
332
+ } catch (error) {
333
+ console.error('Build lock round error:', error);
334
+ res.status(500).json({
335
+ error: error.message || 'Failed to build transaction',
336
+ });
337
+ }
338
+ });
339
+
340
+ /**
341
+ * POST /jackpot/build/resolve
342
+ * Build UNSIGNED transaction for resolving a round
343
+ *
344
+ * Body:
345
+ * {
346
+ * keeperAddress: string (Solana pubkey),
347
+ * roundId?: string (optional, uses current round if not provided)
348
+ * }
349
+ */
350
+ router.post('/build/resolve', async (req, res) => {
351
+ try {
352
+ const { keeperAddress, roundId } = req.body;
353
+
354
+ if (!keeperAddress) {
355
+ return res.status(400).json({
356
+ error: 'Missing required field: keeperAddress'
357
+ });
358
+ }
359
+
360
+ const result = await jackpotService.buildResolveRoundTransaction({
361
+ keeperAddress,
362
+ roundId,
363
+ });
364
+
365
+ res.json(result);
366
+ } catch (error) {
367
+ console.error('Build resolve error:', error);
368
+ res.status(500).json({
369
+ error: error.message || 'Failed to build transaction',
370
+ });
371
+ }
372
+ });
373
+
374
+ /**
375
+ * POST /jackpot/build/reset-round
376
+ * Build UNSIGNED transaction for resetting round (account reuse - Solpot style!)
377
+ * Reuses Round #1 accounts forever instead of creating new ones
378
+ *
379
+ * Body:
380
+ * {
381
+ * keeperAddress: string (Solana pubkey)
382
+ * }
383
+ */
384
+ router.post('/build/reset-round', async (req, res) => {
385
+ try {
386
+ const { keeperAddress } = req.body;
387
+
388
+ if (!keeperAddress) {
389
+ return res.status(400).json({
390
+ error: 'Missing required field: keeperAddress'
391
+ });
392
+ }
393
+
394
+ const result = await jackpotService.buildResetRoundTransaction({
395
+ keeperAddress,
396
+ });
397
+
398
+ res.json(result);
399
+ } catch (error) {
400
+ console.error('Build reset round error:', error);
401
+ res.status(500).json({
402
+ error: error.message || 'Failed to build transaction',
403
+ });
404
+ }
405
+ });
406
+
407
+ // ============================================
408
+ // 🤖 ORACLE ENDPOINTS (Server-side only)
409
+ // ============================================
410
+
411
+ /**
412
+ * POST /jackpot/oracle/reveal
413
+ * Oracle submits random seed (reveal phase)
414
+ *
415
+ * Body:
416
+ * {
417
+ * roundId: string,
418
+ * oracleSeed?: string (hex, optional - auto-generated from Random.org style if not provided)
419
+ * }
420
+ */
421
+ router.post('/oracle/reveal', async (req, res) => {
422
+ try {
423
+ const { roundId } = req.body;
424
+ let { oracleSeed } = req.body;
425
+
426
+ if (!roundId) {
427
+ return res.status(400).json({
428
+ error: 'Missing required field: roundId'
429
+ });
430
+ }
431
+
432
+ // Generate oracle seed if not provided (from Random.org in production)
433
+ if (!oracleSeed) {
434
+ // In production, fetch from Random.org API
435
+ oracleSeed = crypto.randomBytes(32);
436
+ } else {
437
+ oracleSeed = Buffer.from(oracleSeed, 'hex');
438
+ }
439
+
440
+ const result = await jackpotService.consumeRandomness({
441
+ roundId,
442
+ oracleSeed,
443
+ });
444
+
445
+ res.json({
446
+ success: true,
447
+ ...result,
448
+ });
449
+ } catch (error) {
450
+ console.error('Oracle reveal error:', error);
451
+ res.status(500).json({
452
+ error: error.message || 'Failed to reveal randomness',
453
+ });
454
+ }
455
+ });
456
+
457
+ // ============================================
458
+ // 📊 QUERY ENDPOINTS
459
+ // ============================================
460
+
461
+ /**
462
+ * GET /jackpot/config
463
+ * Get protocol configuration
464
+ */
465
+ router.get('/config', async (req, res) => {
466
+ try {
467
+ const config = await jackpotService.getConfig();
468
+
469
+ if (!config) {
470
+ return res.status(404).json({
471
+ error: 'Protocol not initialized'
472
+ });
473
+ }
474
+
475
+ res.json({
476
+ success: true,
477
+ config,
478
+ });
479
+ } catch (error) {
480
+ console.error('Get config error:', error);
481
+ res.status(500).json({
482
+ error: error.message || 'Failed to get config',
483
+ });
484
+ }
485
+ });
486
+
487
+ /**
488
+ * GET /jackpot/round/current
489
+ * Get current round information
490
+ */
491
+ router.get('/round/current', async (req, res) => {
492
+ try {
493
+ const round = await jackpotService.getCurrentRound();
494
+
495
+ if (!round) {
496
+ return res.status(404).json({
497
+ error: 'No active round'
498
+ });
499
+ }
500
+
501
+ res.json({
502
+ success: true,
503
+ round,
504
+ });
505
+ } catch (error) {
506
+ console.error('Get current round error:', error);
507
+ res.status(500).json({
508
+ error: error.message || 'Failed to get current round',
509
+ });
510
+ }
511
+ });
512
+
513
+ /**
514
+ * GET /jackpot/round/:roundId
515
+ * Get specific round information
516
+ */
517
+ router.get('/round/:roundId', async (req, res) => {
518
+ try {
519
+ const { roundId } = req.params;
520
+
521
+ const round = await jackpotService.getRoundInfo(roundId);
522
+
523
+ if (!round) {
524
+ return res.status(404).json({
525
+ error: 'Round not found'
526
+ });
527
+ }
528
+
529
+ res.json({
530
+ success: true,
531
+ round,
532
+ });
533
+ } catch (error) {
534
+ console.error('Get round error:', error);
535
+ res.status(500).json({
536
+ error: error.message || 'Failed to get round',
537
+ });
538
+ }
539
+ });
540
+
541
+ /**
542
+ * GET /jackpot/round/:roundId/verify
543
+ * Get verification data for a round (for provably fair verification)
544
+ * Returns: round data, entries, server seed, oracle seed, signatures
545
+ */
546
+ router.get('/round/:roundId/verify', async (req, res) => {
547
+ try {
548
+ const { roundId } = req.params;
549
+
550
+ // Get round from history database (has verification seeds)
551
+ const historyData = await history.getHistory();
552
+ const historicalRound = historyData.rounds.find(r => r.roundId === roundId);
553
+
554
+ if (!historicalRound) {
555
+ return res.status(404).json({
556
+ error: 'Round not found in history. Only completed rounds can be verified.'
557
+ });
558
+ }
559
+
560
+ // Get entries for weight verification (current round entries)
561
+ let entries = [];
562
+ try {
563
+ const entriesRes = await jackpotService.getRoundEntries(1); // Always round 1 PDA
564
+ entries = entriesRes?.entries || [];
565
+ } catch (e) {
566
+ console.log('Could not fetch entries (expected for old rounds)');
567
+ }
568
+
569
+ res.json({
570
+ success: true,
571
+ verification: {
572
+ roundId: historicalRound.roundId,
573
+ winner: historicalRound.winner,
574
+ winAmount: historicalRound.winAmount,
575
+ totalPot: historicalRound.totalPot,
576
+ entryCount: historicalRound.entryCount,
577
+ timestamp: historicalRound.timestamp,
578
+ signature: historicalRound.signature,
579
+
580
+ // Provably fair verification data
581
+ serverSeed: historicalRound.serverSeed,
582
+ serverSeedHash: historicalRound.serverSeedHash,
583
+ oracleSeed: historicalRound.oracleSeed,
584
+
585
+ // Entries for weight verification
586
+ entries,
587
+ }
588
+ });
589
+ } catch (error) {
590
+ console.error('Get verification data error:', error);
591
+ res.status(500).json({
592
+ error: error.message || 'Failed to get verification data',
593
+ });
594
+ }
595
+ });
596
+
597
+ /**
598
+ * GET /jackpot/round/:roundId/entries
599
+ * Get all entries for a round
600
+ * NOTE: With account reuse, we always query Round 1 PDAs!
601
+ */
602
+ router.get('/round/:roundId/entries', async (req, res) => {
603
+ try {
604
+ const { roundId } = req.params;
605
+
606
+ // Always use Round 1 PDA for entries (account reuse!)
607
+ const entries = await jackpotService.getRoundEntries(1);
608
+ const round = await jackpotService.getRoundInfo(1);
609
+
610
+ // Calculate odds for each entry
611
+ const totalWeight = round ? BigInt(round.totalWeight) : 0n;
612
+
613
+ const entriesWithOdds = entries.map(entry => ({
614
+ ...entry,
615
+ oddsPercent: totalWeight > 0n
616
+ ? (Number(BigInt(entry.weight) * 10000n / totalWeight) / 100).toFixed(2)
617
+ : '0.00',
618
+ }));
619
+
620
+ res.json({
621
+ success: true,
622
+ roundId: round ? round.roundId : roundId.toString(), // Return actual round ID from account
623
+ entries: entriesWithOdds,
624
+ totalEntries: entries.length,
625
+ });
626
+ } catch (error) {
627
+ console.error('Get entries error:', error);
628
+ res.status(500).json({
629
+ error: error.message || 'Failed to get entries',
630
+ });
631
+ }
632
+ });
633
+
634
+ /**
635
+ * GET /jackpot/stats
636
+ * Get overall jackpot statistics
637
+ */
638
+ router.get('/stats', async (req, res) => {
639
+ try {
640
+ const stats = await jackpotService.getStats();
641
+
642
+ res.json({
643
+ success: true,
644
+ ...stats,
645
+ });
646
+ } catch (error) {
647
+ console.error('Get stats error:', error);
648
+ res.status(500).json({
649
+ error: error.message || 'Failed to get stats',
650
+ });
651
+ }
652
+ });
653
+
654
+ /**
655
+ * GET /jackpot/winner/last
656
+ * Get last winner information
657
+ */
658
+ router.get('/winner/last', async (req, res) => {
659
+ try {
660
+ const lastWinner = await history.getLastWinner();
661
+
662
+ if (!lastWinner) {
663
+ return res.json({
664
+ success: true,
665
+ winner: null,
666
+ message: 'No winners yet'
667
+ });
668
+ }
669
+
670
+ res.json({
671
+ success: true,
672
+ winner: lastWinner,
673
+ });
674
+ } catch (error) {
675
+ console.error('Get last winner error:', error);
676
+ res.status(500).json({
677
+ error: error.message || 'Failed to get last winner',
678
+ });
679
+ }
680
+ });
681
+
682
+ /**
683
+ * GET /jackpot/history
684
+ * Get round history
685
+ */
686
+ router.get('/history', async (req, res) => {
687
+ try {
688
+ const limit = parseInt(req.query.limit) || 10;
689
+ const historyData = await history.getHistory();
690
+
691
+ res.json({
692
+ success: true,
693
+ rounds: historyData.rounds.slice(0, limit),
694
+ total: historyData.rounds.length,
695
+ });
696
+ } catch (error) {
697
+ console.error('Get history error:', error);
698
+ res.status(500).json({
699
+ error: error.message || 'Failed to get history',
700
+ });
701
+ }
702
+ });
703
+
704
+ /**
705
+ * GET /jackpot/history/wallet/:walletAddress
706
+ * Get jackpot entries and wins for a specific wallet (for transaction history linking)
707
+ */
708
+ router.get('/history/wallet/:walletAddress', async (req, res) => {
709
+ try {
710
+ const { walletAddress } = req.params;
711
+
712
+ if (!walletAddress || walletAddress.length < 32) {
713
+ return res.status(400).json({ error: 'Invalid wallet address' });
714
+ }
715
+
716
+ const [entries, wins] = await Promise.all([
717
+ history.getEntriesByWallet(walletAddress),
718
+ history.getWinsByWallet(walletAddress),
719
+ ]);
720
+
721
+ res.json({
722
+ success: true,
723
+ entries,
724
+ wins,
725
+ });
726
+ } catch (error) {
727
+ console.error('Get wallet jackpot history error:', error);
728
+ res.status(500).json({
729
+ error: error.message || 'Failed to get wallet jackpot history',
730
+ });
731
+ }
732
+ });
733
+
734
+ // ============================================
735
+ // 🏥 HEALTH ENDPOINT
736
+ // ============================================
737
+
738
+ /**
739
+ * GET /jackpot/health
740
+ * Health check for jackpot service
741
+ */
742
+ router.get('/health', (req, res) => {
743
+ res.json({
744
+ status: 'healthy',
745
+ service: 'jackpot',
746
+ programId: jackpotService.programId.toString(),
747
+ oracleWallet: jackpotService.oracleWallet.toString(),
748
+ timestamp: new Date().toISOString(),
749
+ });
750
+ });
751
+
752
+ return router;
753
+ };
754
+