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,623 @@
1
+ /**
2
+ * Survivor Pool Controller
3
+ * Business logic for March Madness Survivor Pool feature
4
+ */
5
+
6
+ const { pool } = require('../services/db');
7
+
8
+ const LAMPORTS_PER_SOL = 1_000_000_000;
9
+
10
+ // Round names for display
11
+ const ROUND_NAMES = {
12
+ 1: 'First Four',
13
+ 2: 'Round of 64',
14
+ 3: 'Round of 32',
15
+ 4: 'Sweet 16',
16
+ 5: 'Elite 8',
17
+ 6: 'Final Four',
18
+ 7: 'Championship'
19
+ };
20
+
21
+ /**
22
+ * Create a new survivor pool
23
+ */
24
+ async function createPool({
25
+ name,
26
+ year,
27
+ buyInLamports,
28
+ roundDeadline,
29
+ allowSameTeamTwice = false,
30
+ solanaGameId = null,
31
+ createdBy
32
+ }) {
33
+ const result = await pool.query(
34
+ `INSERT INTO survivor_pools
35
+ (name, year, buy_in_lamports, round_deadline, allow_same_team_twice, solana_game_id, created_by)
36
+ VALUES ($1, $2, $3, $4, $5, $6, $7)
37
+ RETURNING *`,
38
+ [name, year, buyInLamports, roundDeadline, allowSameTeamTwice, solanaGameId, createdBy]
39
+ );
40
+ return result.rows[0];
41
+ }
42
+
43
+ /**
44
+ * Get all pools with optional status filter
45
+ */
46
+ async function getPools({ status, year } = {}) {
47
+ let query = `
48
+ SELECT
49
+ sp.*,
50
+ (SELECT COUNT(*) FROM survivor_entries WHERE pool_id = sp.id) as total_entries,
51
+ (SELECT COUNT(*) FROM survivor_entries WHERE pool_id = sp.id AND is_alive = true) as alive_count
52
+ FROM survivor_pools sp
53
+ WHERE 1=1
54
+ `;
55
+ const params = [];
56
+
57
+ if (status) {
58
+ params.push(status);
59
+ query += ` AND sp.status = $${params.length}`;
60
+ }
61
+
62
+ if (year) {
63
+ params.push(year);
64
+ query += ` AND sp.year = $${params.length}`;
65
+ }
66
+
67
+ query += ' ORDER BY sp.created_at DESC';
68
+
69
+ const result = await pool.query(query, params);
70
+ return result.rows.map(formatPool);
71
+ }
72
+
73
+ /**
74
+ * Get a single pool by ID with detailed stats
75
+ */
76
+ async function getPoolById(poolId) {
77
+ const result = await pool.query(
78
+ `SELECT
79
+ sp.*,
80
+ (SELECT COUNT(*) FROM survivor_entries WHERE pool_id = sp.id) as total_entries,
81
+ (SELECT COUNT(*) FROM survivor_entries WHERE pool_id = sp.id AND is_alive = true) as alive_count,
82
+ (SELECT SUM(buy_in_lamports) FROM survivor_pools WHERE id = sp.id) *
83
+ (SELECT COUNT(*) FROM survivor_entries WHERE pool_id = sp.id) as total_pot_lamports
84
+ FROM survivor_pools sp
85
+ WHERE sp.id = $1`,
86
+ [poolId]
87
+ );
88
+
89
+ if (result.rows.length === 0) {
90
+ return null;
91
+ }
92
+
93
+ return formatPool(result.rows[0]);
94
+ }
95
+
96
+ /**
97
+ * Update pool (admin)
98
+ */
99
+ async function updatePool(poolId, updates) {
100
+ const allowedFields = ['name', 'current_round', 'round_deadline', 'status', 'solana_game_id'];
101
+ const setClause = [];
102
+ const params = [poolId];
103
+
104
+ for (const [key, value] of Object.entries(updates)) {
105
+ const snakeKey = key.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
106
+ if (allowedFields.includes(snakeKey)) {
107
+ params.push(value);
108
+ setClause.push(`${snakeKey} = $${params.length}`);
109
+ }
110
+ }
111
+
112
+ if (setClause.length === 0) {
113
+ throw new Error('No valid fields to update');
114
+ }
115
+
116
+ setClause.push('updated_at = NOW()');
117
+
118
+ const result = await pool.query(
119
+ `UPDATE survivor_pools SET ${setClause.join(', ')} WHERE id = $1 RETURNING *`,
120
+ params
121
+ );
122
+
123
+ return result.rows[0] ? formatPool(result.rows[0]) : null;
124
+ }
125
+
126
+ /**
127
+ * Join a survivor pool
128
+ */
129
+ async function joinPool({ poolId, userId, walletAddress, txSignature }) {
130
+ // Check pool exists and is open
131
+ const poolResult = await pool.query(
132
+ 'SELECT * FROM survivor_pools WHERE id = $1',
133
+ [poolId]
134
+ );
135
+
136
+ if (poolResult.rows.length === 0) {
137
+ throw new Error('Pool not found');
138
+ }
139
+
140
+ const survivorPool = poolResult.rows[0];
141
+
142
+ // Allow joining when pool is 'open' (before bracket)
143
+ // Or 'active' but only if no games have been completed yet (tournament hasn't really started)
144
+ if (survivorPool.status === 'open') {
145
+ // Always allow when open
146
+ console.log(`[Survivor] Pool ${poolId} is open, allowing join`);
147
+ } else if (survivorPool.status === 'active') {
148
+ // Check if any games have been completed
149
+ const completedGames = await pool.query(
150
+ `SELECT COUNT(*) as count FROM survivor_tournament_games
151
+ WHERE pool_id = $1 AND status = 'final'`,
152
+ [poolId]
153
+ );
154
+
155
+ const completedCount = parseInt(completedGames.rows[0].count);
156
+ console.log(`[Survivor] Pool ${poolId} is active, completed games: ${completedCount}`);
157
+
158
+ if (completedCount > 0) {
159
+ throw new Error(`Tournament has started (${completedCount} games completed). Reset the pool to allow new entries.`);
160
+ }
161
+ } else {
162
+ throw new Error(`Pool is ${survivorPool.status}, cannot join`);
163
+ }
164
+
165
+ // Check if user already joined
166
+ const existingEntry = await pool.query(
167
+ 'SELECT id FROM survivor_entries WHERE pool_id = $1 AND user_id = $2',
168
+ [poolId, userId]
169
+ );
170
+
171
+ if (existingEntry.rows.length > 0) {
172
+ throw new Error('Already joined this pool');
173
+ }
174
+
175
+ // Create entry
176
+ const result = await pool.query(
177
+ `INSERT INTO survivor_entries (pool_id, user_id, wallet_address, entry_tx_signature)
178
+ VALUES ($1, $2, $3, $4)
179
+ RETURNING *`,
180
+ [poolId, userId, walletAddress, txSignature]
181
+ );
182
+
183
+ return result.rows[0];
184
+ }
185
+
186
+ /**
187
+ * Get user's entry in a pool
188
+ */
189
+ async function getUserEntry(poolId, userId) {
190
+ const result = await pool.query(
191
+ `SELECT
192
+ se.*,
193
+ u.username,
194
+ u.avatar
195
+ FROM survivor_entries se
196
+ JOIN users u ON u.id = se.user_id
197
+ WHERE se.pool_id = $1 AND se.user_id = $2`,
198
+ [poolId, userId]
199
+ );
200
+
201
+ if (result.rows.length === 0) {
202
+ return null;
203
+ }
204
+
205
+ const entry = result.rows[0];
206
+
207
+ // Get all picks for this entry
208
+ const picksResult = await pool.query(
209
+ `SELECT * FROM survivor_picks WHERE entry_id = $1 ORDER BY round`,
210
+ [entry.id]
211
+ );
212
+
213
+ return {
214
+ ...entry,
215
+ picks: picksResult.rows.map(formatPick)
216
+ };
217
+ }
218
+
219
+ /**
220
+ * Get available teams for current round
221
+ * Returns teams playing in the current round, marking previously picked teams as unavailable
222
+ */
223
+ async function getAvailableTeams(poolId, userId) {
224
+ // Get pool info
225
+ const poolResult = await pool.query(
226
+ 'SELECT * FROM survivor_pools WHERE id = $1',
227
+ [poolId]
228
+ );
229
+
230
+ if (poolResult.rows.length === 0) {
231
+ throw new Error('Pool not found');
232
+ }
233
+
234
+ const survivorPool = poolResult.rows[0];
235
+ const currentRound = survivorPool.current_round;
236
+
237
+ // Get user's entry
238
+ const entryResult = await pool.query(
239
+ 'SELECT id FROM survivor_entries WHERE pool_id = $1 AND user_id = $2',
240
+ [poolId, userId]
241
+ );
242
+
243
+ if (entryResult.rows.length === 0) {
244
+ throw new Error('Not joined this pool');
245
+ }
246
+
247
+ const entryId = entryResult.rows[0].id;
248
+
249
+ // Get previously picked teams (to mark as unavailable)
250
+ const previousPicksResult = await pool.query(
251
+ 'SELECT team_id FROM survivor_picks WHERE entry_id = $1',
252
+ [entryId]
253
+ );
254
+ const pickedTeamIds = new Set(previousPicksResult.rows.map(r => r.team_id));
255
+
256
+ // Get all games for current round
257
+ const gamesResult = await pool.query(
258
+ `SELECT * FROM survivor_tournament_games
259
+ WHERE pool_id = $1 AND round = $2
260
+ ORDER BY game_date, id`,
261
+ [poolId, currentRound]
262
+ );
263
+
264
+ console.log(`[Survivor] getAvailableTeams: poolId=${poolId}, currentRound=${currentRound}, games found: ${gamesResult.rows.length}`);
265
+ if (gamesResult.rows.length === 0) {
266
+ // Debug: check what rounds exist
267
+ const allGames = await pool.query(
268
+ `SELECT round, COUNT(*) as count FROM survivor_tournament_games WHERE pool_id = $1 GROUP BY round ORDER BY round`,
269
+ [poolId]
270
+ );
271
+ console.log(`[Survivor] Available rounds in DB:`, allGames.rows);
272
+ }
273
+
274
+ // Build team list with availability
275
+ const teams = [];
276
+ for (const game of gamesResult.rows) {
277
+ const team1Available = !pickedTeamIds.has(game.team1_id) || survivorPool.allow_same_team_twice;
278
+ const team2Available = !pickedTeamIds.has(game.team2_id) || survivorPool.allow_same_team_twice;
279
+
280
+ teams.push({
281
+ teamId: game.team1_id,
282
+ teamName: game.team1_name,
283
+ seed: game.team1_seed,
284
+ logo: game.team1_logo,
285
+ available: team1Available,
286
+ espnGameId: game.espn_game_id,
287
+ gameDate: game.game_date,
288
+ opponent: {
289
+ teamId: game.team2_id,
290
+ teamName: game.team2_name,
291
+ seed: game.team2_seed
292
+ },
293
+ region: game.region
294
+ });
295
+
296
+ teams.push({
297
+ teamId: game.team2_id,
298
+ teamName: game.team2_name,
299
+ seed: game.team2_seed,
300
+ logo: game.team2_logo,
301
+ available: team2Available,
302
+ espnGameId: game.espn_game_id,
303
+ gameDate: game.game_date,
304
+ opponent: {
305
+ teamId: game.team1_id,
306
+ teamName: game.team1_name,
307
+ seed: game.team1_seed
308
+ },
309
+ region: game.region
310
+ });
311
+ }
312
+
313
+ return {
314
+ round: currentRound,
315
+ roundName: ROUND_NAMES[currentRound],
316
+ deadline: survivorPool.round_deadline,
317
+ teams
318
+ };
319
+ }
320
+
321
+ /**
322
+ * Submit a pick for the current round
323
+ */
324
+ async function submitPick({ poolId, userId, teamId, teamName, teamSeed, teamLogo, espnGameId, opponentName, opponentSeed }) {
325
+ // Get pool
326
+ const poolResult = await pool.query(
327
+ 'SELECT * FROM survivor_pools WHERE id = $1',
328
+ [poolId]
329
+ );
330
+
331
+ if (poolResult.rows.length === 0) {
332
+ throw new Error('Pool not found');
333
+ }
334
+
335
+ const survivorPool = poolResult.rows[0];
336
+
337
+ // Check pool is active
338
+ if (survivorPool.status !== 'active' && survivorPool.status !== 'open') {
339
+ throw new Error(`Pool is ${survivorPool.status}, cannot submit picks`);
340
+ }
341
+
342
+ // Check deadline
343
+ if (survivorPool.round_deadline && new Date() > new Date(survivorPool.round_deadline)) {
344
+ throw new Error('Pick deadline has passed');
345
+ }
346
+
347
+ // Get user's entry
348
+ const entryResult = await pool.query(
349
+ 'SELECT * FROM survivor_entries WHERE pool_id = $1 AND user_id = $2',
350
+ [poolId, userId]
351
+ );
352
+
353
+ if (entryResult.rows.length === 0) {
354
+ throw new Error('Not joined this pool');
355
+ }
356
+
357
+ const entry = entryResult.rows[0];
358
+
359
+ // Check if eliminated
360
+ if (!entry.is_alive) {
361
+ throw new Error('You have been eliminated from this pool');
362
+ }
363
+
364
+ // Check if team was already picked (if not allowed)
365
+ if (!survivorPool.allow_same_team_twice) {
366
+ const existingPick = await pool.query(
367
+ 'SELECT id FROM survivor_picks WHERE entry_id = $1 AND team_id = $2',
368
+ [entry.id, teamId]
369
+ );
370
+
371
+ if (existingPick.rows.length > 0) {
372
+ throw new Error('You have already picked this team in a previous round');
373
+ }
374
+ }
375
+
376
+ // Upsert pick for current round (allows changing pick before deadline)
377
+ const result = await pool.query(
378
+ `INSERT INTO survivor_picks
379
+ (entry_id, round, team_id, team_name, team_seed, team_logo, espn_game_id, opponent_name, opponent_seed)
380
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
381
+ ON CONFLICT (entry_id, round)
382
+ DO UPDATE SET
383
+ team_id = EXCLUDED.team_id,
384
+ team_name = EXCLUDED.team_name,
385
+ team_seed = EXCLUDED.team_seed,
386
+ team_logo = EXCLUDED.team_logo,
387
+ espn_game_id = EXCLUDED.espn_game_id,
388
+ opponent_name = EXCLUDED.opponent_name,
389
+ opponent_seed = EXCLUDED.opponent_seed,
390
+ updated_at = NOW()
391
+ RETURNING *`,
392
+ [entry.id, survivorPool.current_round, teamId, teamName, teamSeed, teamLogo, espnGameId, opponentName, opponentSeed]
393
+ );
394
+
395
+ return formatPick(result.rows[0]);
396
+ }
397
+
398
+ /**
399
+ * Get all picks for a user's entry
400
+ */
401
+ async function getUserPicks(poolId, userId) {
402
+ const result = await pool.query(
403
+ `SELECT sp.*
404
+ FROM survivor_picks sp
405
+ JOIN survivor_entries se ON se.id = sp.entry_id
406
+ WHERE se.pool_id = $1 AND se.user_id = $2
407
+ ORDER BY sp.round`,
408
+ [poolId, userId]
409
+ );
410
+
411
+ return result.rows.map(formatPick);
412
+ }
413
+
414
+ /**
415
+ * Get leaderboard - surviving entries
416
+ */
417
+ async function getAliveEntries(poolId) {
418
+ // Get current round first
419
+ const poolResult = await pool.query(
420
+ `SELECT current_round FROM survivor_pools WHERE id = $1`,
421
+ [poolId]
422
+ );
423
+ const currentRound = poolResult.rows[0]?.current_round || 1;
424
+
425
+ const result = await pool.query(
426
+ `SELECT
427
+ se.*,
428
+ u.username,
429
+ u.avatar,
430
+ (SELECT COUNT(*) FROM survivor_picks WHERE entry_id = se.id AND result = 'won') as wins,
431
+ sp.team_name as current_pick_team,
432
+ sp.team_logo as current_pick_logo,
433
+ sp.team_id as current_pick_team_id
434
+ FROM survivor_entries se
435
+ JOIN users u ON u.id = se.user_id
436
+ LEFT JOIN survivor_picks sp ON sp.entry_id = se.id AND sp.round = $2
437
+ WHERE se.pool_id = $1 AND se.is_alive = true
438
+ ORDER BY wins DESC, se.created_at`,
439
+ [poolId, currentRound]
440
+ );
441
+
442
+ return result.rows;
443
+ }
444
+
445
+ /**
446
+ * Get eliminated entries
447
+ */
448
+ async function getEliminatedEntries(poolId) {
449
+ const result = await pool.query(
450
+ `SELECT
451
+ se.*,
452
+ u.username,
453
+ u.avatar,
454
+ sp.team_name as eliminated_by_pick,
455
+ sp.team_logo as eliminated_by_logo,
456
+ sp.team_id as eliminated_by_team_id,
457
+ (SELECT COUNT(*) FROM survivor_picks WHERE entry_id = se.id AND result = 'won') as wins
458
+ FROM survivor_entries se
459
+ JOIN users u ON u.id = se.user_id
460
+ LEFT JOIN survivor_picks sp ON sp.entry_id = se.id AND sp.round = se.eliminated_at_round
461
+ WHERE se.pool_id = $1 AND se.is_alive = false
462
+ ORDER BY se.eliminated_at_round DESC, se.updated_at DESC`,
463
+ [poolId]
464
+ );
465
+
466
+ return result.rows;
467
+ }
468
+
469
+ /**
470
+ * Get pool stats summary
471
+ */
472
+ async function getPoolStats(poolId) {
473
+ const result = await pool.query(
474
+ `SELECT
475
+ (SELECT COUNT(*) FROM survivor_entries WHERE pool_id = $1) as total_entries,
476
+ (SELECT COUNT(*) FROM survivor_entries WHERE pool_id = $1 AND is_alive = true) as alive_count,
477
+ (SELECT COUNT(*) FROM survivor_entries WHERE pool_id = $1 AND is_alive = false) as eliminated_count,
478
+ (SELECT buy_in_lamports FROM survivor_pools WHERE id = $1) as buy_in_lamports,
479
+ (SELECT current_round FROM survivor_pools WHERE id = $1) as current_round,
480
+ (SELECT status FROM survivor_pools WHERE id = $1) as status
481
+ `,
482
+ [poolId]
483
+ );
484
+
485
+ const stats = result.rows[0];
486
+ const totalPotLamports = BigInt(stats.buy_in_lamports || 0) * BigInt(stats.total_entries || 0);
487
+ const feePercentage = 6; // 6% total fee
488
+ const netPotLamports = totalPotLamports - (totalPotLamports * BigInt(feePercentage) / BigInt(100));
489
+
490
+ return {
491
+ totalEntries: parseInt(stats.total_entries) || 0,
492
+ aliveCount: parseInt(stats.alive_count) || 0,
493
+ eliminatedCount: parseInt(stats.eliminated_count) || 0,
494
+ currentRound: stats.current_round,
495
+ roundName: ROUND_NAMES[stats.current_round],
496
+ status: stats.status,
497
+ totalPotLamports: totalPotLamports.toString(),
498
+ totalPotSol: Number(totalPotLamports) / LAMPORTS_PER_SOL,
499
+ netPotLamports: netPotLamports.toString(),
500
+ netPotSol: Number(netPotLamports) / LAMPORTS_PER_SOL,
501
+ potentialWinningsPerSurvivor: stats.alive_count > 0
502
+ ? Number(netPotLamports) / LAMPORTS_PER_SOL / parseInt(stats.alive_count)
503
+ : 0
504
+ };
505
+ }
506
+
507
+ /**
508
+ * Import tournament games from ESPN data
509
+ */
510
+ async function importTournamentGames(poolId, games) {
511
+ const insertedGames = [];
512
+
513
+ for (const game of games) {
514
+ const result = await pool.query(
515
+ `INSERT INTO survivor_tournament_games
516
+ (pool_id, round, espn_game_id, game_date, team1_id, team1_name, team1_seed, team1_logo,
517
+ team2_id, team2_name, team2_seed, team2_logo, region, status)
518
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
519
+ ON CONFLICT (pool_id, espn_game_id) DO UPDATE SET
520
+ game_date = EXCLUDED.game_date,
521
+ team1_name = EXCLUDED.team1_name,
522
+ team2_name = EXCLUDED.team2_name,
523
+ status = EXCLUDED.status,
524
+ updated_at = NOW()
525
+ RETURNING *`,
526
+ [
527
+ poolId,
528
+ game.round,
529
+ game.espnGameId,
530
+ game.gameDate,
531
+ game.team1Id,
532
+ game.team1Name,
533
+ game.team1Seed,
534
+ game.team1Logo,
535
+ game.team2Id,
536
+ game.team2Name,
537
+ game.team2Seed,
538
+ game.team2Logo,
539
+ game.region,
540
+ game.status || 'scheduled'
541
+ ]
542
+ );
543
+
544
+ insertedGames.push(result.rows[0]);
545
+ }
546
+
547
+ return insertedGames;
548
+ }
549
+
550
+ /**
551
+ * Update tournament game result
552
+ */
553
+ async function updateGameResult(poolId, espnGameId, { winnerId, team1Score, team2Score, status }) {
554
+ const result = await pool.query(
555
+ `UPDATE survivor_tournament_games
556
+ SET winner_id = $3, team1_score = $4, team2_score = $5, status = $6, updated_at = NOW()
557
+ WHERE pool_id = $1 AND espn_game_id = $2
558
+ RETURNING *`,
559
+ [poolId, espnGameId, winnerId, team1Score, team2Score, status]
560
+ );
561
+
562
+ return result.rows[0];
563
+ }
564
+
565
+ // Helper function to format pick for API response
566
+ function formatPick(row) {
567
+ return {
568
+ id: row.id,
569
+ entryId: row.entry_id,
570
+ round: row.round,
571
+ teamId: row.team_id,
572
+ teamName: row.team_name,
573
+ teamLogo: row.team_logo,
574
+ espnGameId: row.espn_game_id,
575
+ opponentName: row.opponent_name,
576
+ opponentSeed: row.opponent_seed,
577
+ result: row.result,
578
+ createdAt: row.created_at,
579
+ updatedAt: row.updated_at
580
+ };
581
+ }
582
+
583
+ // Helper function to format pool for API response
584
+ function formatPool(row) {
585
+ return {
586
+ id: row.id,
587
+ name: row.name,
588
+ year: row.year,
589
+ buyInLamports: row.buy_in_lamports,
590
+ buyInSol: Number(row.buy_in_lamports) / LAMPORTS_PER_SOL,
591
+ currentRound: row.current_round,
592
+ roundName: ROUND_NAMES[row.current_round],
593
+ roundDeadline: row.round_deadline,
594
+ status: row.status,
595
+ allowSameTeamTwice: row.allow_same_team_twice,
596
+ solanaGameId: row.solana_game_id,
597
+ createdBy: row.created_by,
598
+ createdAt: row.created_at,
599
+ updatedAt: row.updated_at,
600
+ entryCount: parseInt(row.total_entries) || 0,
601
+ aliveCount: parseInt(row.alive_count) || 0,
602
+ totalPotLamports: row.total_pot_lamports,
603
+ totalPotSol: row.total_pot_lamports ? Number(row.total_pot_lamports) / LAMPORTS_PER_SOL : undefined
604
+ };
605
+ }
606
+
607
+ module.exports = {
608
+ createPool,
609
+ getPools,
610
+ getPoolById,
611
+ updatePool,
612
+ joinPool,
613
+ getUserEntry,
614
+ getAvailableTeams,
615
+ submitPick,
616
+ getUserPicks,
617
+ getAliveEntries,
618
+ getEliminatedEntries,
619
+ getPoolStats,
620
+ importTournamentGames,
621
+ updateGameResult,
622
+ ROUND_NAMES
623
+ };
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Oracle Monitor - Cron job for automatic sports game resolution
4
+ * Checks pending games every 1 minute and resolves completed ones
5
+ */
6
+
7
+ require('dotenv').config();
8
+ const { Keypair } = require('@solana/web3.js');
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const AutomaticGameOracle = require('../services/automaticGameOracle');
12
+
13
+ // Load oracle wallet
14
+ function loadOracleWallet() {
15
+ // Try environment variable first (for Heroku)
16
+ if (process.env.ORACLE_WALLET_JSON) {
17
+ console.log('šŸ”‘ Loading oracle wallet from environment variable');
18
+ const secretKey = JSON.parse(process.env.ORACLE_WALLET_JSON);
19
+ return Keypair.fromSecretKey(Uint8Array.from(secretKey));
20
+ }
21
+
22
+ // Fallback to file (for local development)
23
+ const oracleWalletPath = process.env.ORACLE_WALLET_PATH || path.join(__dirname, '../wallets/oracle.json');
24
+
25
+ if (!fs.existsSync(oracleWalletPath)) {
26
+ console.error(`āŒ Oracle wallet not found at: ${oracleWalletPath}`);
27
+ console.log('šŸ’” Create one with: solana-keygen new -o wallets/oracle.json');
28
+ console.log('šŸ’” Or set ORACLE_WALLET_JSON environment variable');
29
+ process.exit(1);
30
+ }
31
+
32
+ console.log('šŸ”‘ Loading oracle wallet from file:', oracleWalletPath);
33
+ const secretKey = JSON.parse(fs.readFileSync(oracleWalletPath, 'utf-8'));
34
+ return Keypair.fromSecretKey(Uint8Array.from(secretKey));
35
+ }
36
+
37
+ // Configuration
38
+ const config = {
39
+ rpcUrl: process.env.SOLANA_NETWORK || 'https://api.devnet.solana.com',
40
+ programId: process.env.PROGRAM_ID || '8DJTkgk6MDr6tPtw4v2VzYAz9WWvmCg6786vZrEK3o5q',
41
+ oracleKeypair: loadOracleWallet(),
42
+ liveScoresApiUrl: process.env.LIVE_SCORES_API_URL || 'http://localhost:3002',
43
+ // PostgreSQL dubs-server URL - Use PORT-based URL for local dev, explicit URL for production
44
+ dubsServerUrl: process.env.DUBS_SERVER_URL || `http://localhost:${process.env.PORT || 3001}`,
45
+ checkIntervalMs: parseInt(process.env.ORACLE_CHECK_INTERVAL || '60000'), // 1 minute
46
+ notifyBeforeMinutes: parseInt(process.env.NOTIFY_BEFORE_MINUTES || '10'), // Notify 10 minutes before
47
+ };
48
+
49
+ console.log('šŸ¤– Initializing Oracle Monitor...');
50
+ console.log(` RPC: ${config.rpcUrl}`);
51
+ console.log(` Program ID: ${config.programId}`);
52
+ console.log(` Oracle: ${config.oracleKeypair.publicKey.toString()}`);
53
+ console.log(` PostgreSQL: ${config.dubsServerUrl}`);
54
+ console.log(` Live Scores API: ${config.liveScoresApiUrl}`);
55
+ console.log(` Check interval: ${config.checkIntervalMs / 1000}s`);
56
+ console.log(` Notify before: ${config.notifyBeforeMinutes} minutes`);
57
+
58
+ // Create and start oracle
59
+ const oracle = new AutomaticGameOracle(config);
60
+ oracle.start();
61
+
62
+ // Graceful shutdown
63
+ process.on('SIGINT', () => {
64
+ console.log('\nšŸ‘‹ Shutting down oracle monitor...');
65
+ oracle.stop();
66
+ process.exit(0);
67
+ });
68
+
69
+ process.on('SIGTERM', () => {
70
+ console.log('\nšŸ‘‹ Shutting down oracle monitor...');
71
+ oracle.stop();
72
+ process.exit(0);
73
+ });
74
+
75
+ console.log('āœ… Oracle monitor started successfully!');
76
+ console.log(' Press Ctrl+C to stop\n');
77
+