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,373 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Check Game Status Script
4
+ * Shows all players, their picks, win/loss status, and claim status
5
+ *
6
+ * Usage:
7
+ * node scripts/check-game-status.js <gameId>
8
+ *
9
+ * Examples:
10
+ * node scripts/check-game-status.js sport-1769329525013-mmekk2qqd
11
+ *
12
+ * Environment:
13
+ * DATABASE_URL - PostgreSQL connection string (uses Heroku if not set)
14
+ */
15
+
16
+ const { Pool } = require('pg');
17
+
18
+ // Get database URL from environment or use Heroku
19
+ const DATABASE_URL = process.env.DATABASE_URL;
20
+
21
+ if (!DATABASE_URL) {
22
+ console.error('❌ DATABASE_URL not set. Run with:');
23
+ console.error(' heroku config:get DATABASE_URL -a dubs-server-prod | xargs -I {} DATABASE_URL={} node scripts/check-game-status.js <gameId>');
24
+ console.error('');
25
+ console.error('Or for a one-liner:');
26
+ console.error(' DATABASE_URL=$(heroku config:get DATABASE_URL -a dubs-server-prod) node scripts/check-game-status.js <gameId>');
27
+ process.exit(1);
28
+ }
29
+
30
+ const pool = new Pool({
31
+ connectionString: DATABASE_URL,
32
+ ssl: { rejectUnauthorized: false }
33
+ });
34
+
35
+ async function checkGameStatus(gameId) {
36
+ console.log('');
37
+ console.log('🎮 Game Status Check');
38
+ console.log('====================');
39
+ console.log(`Game ID: ${gameId}`);
40
+ console.log('');
41
+
42
+ try {
43
+ // Get game data
44
+ const gameResult = await pool.query(`
45
+ SELECT
46
+ g.game_id,
47
+ g.buy_in,
48
+ g.is_resolved,
49
+ g.home_team_players,
50
+ g.away_team_players,
51
+ g.draw_team_players,
52
+ g.player_amounts,
53
+ g.sports_event->>'strHomeTeam' as home_team,
54
+ g.sports_event->>'strAwayTeam' as away_team,
55
+ g.sports_event->'finalScore'->>'winner' as winner,
56
+ g.sports_event->'finalScore'->>'homeScore' as home_score,
57
+ g.sports_event->'finalScore'->>'awayScore' as away_score
58
+ FROM games g
59
+ WHERE g.game_id = $1
60
+ `, [gameId]);
61
+
62
+ if (gameResult.rows.length === 0) {
63
+ console.error('❌ Game not found');
64
+ process.exit(1);
65
+ }
66
+
67
+ const game = gameResult.rows[0];
68
+
69
+ // Display game info
70
+ const homeTeam = game.home_team || 'Home';
71
+ const awayTeam = game.away_team || 'Away';
72
+
73
+ console.log('📊 Game Info');
74
+ console.log('------------');
75
+ console.log(`${homeTeam} vs ${awayTeam}`);
76
+ if (game.is_resolved && !game.winner) {
77
+ console.log(`Score: ${game.home_score || 0} - ${game.away_score || 0}`);
78
+ console.log('Result: REFUND 💰 (no competition)');
79
+ } else if (game.winner) {
80
+ console.log(`Score: ${game.home_score} - ${game.away_score}`);
81
+ console.log(`Winner: ${game.winner === 'home' ? homeTeam : game.winner === 'away' ? awayTeam : 'Draw'}`);
82
+ } else {
83
+ console.log('Status: Not resolved yet');
84
+ }
85
+ console.log(`Buy-in: ${parseFloat(game.buy_in).toFixed(4)} SOL`);
86
+ console.log(`Resolved: ${game.is_resolved ? 'Yes ✅' : 'No ⏳'}`);
87
+ console.log('');
88
+
89
+ // Get all participants with their claim status + promo code info
90
+ const participantsResult = await pool.query(`
91
+ SELECT
92
+ ugr.wallet_address,
93
+ u.username,
94
+ ugr.team_choice,
95
+ ugr.claimed_at,
96
+ ugr.claim_signature,
97
+ ugr.amount_claimed,
98
+ pc.code as promo_code
99
+ FROM user_game_refs ugr
100
+ LEFT JOIN users u ON ugr.wallet_address = u.wallet_address
101
+ LEFT JOIN promo_codes pc ON pc.used_by = ugr.wallet_address AND pc.used_in_game = ugr.game_id
102
+ WHERE ugr.game_id = $1
103
+ ORDER BY ugr.team_choice, ugr.joined_at
104
+ `, [gameId]);
105
+
106
+ if (participantsResult.rows.length === 0) {
107
+ console.log('❌ No participants found');
108
+ process.exit(1);
109
+ }
110
+
111
+ // Calculate totals - support both legacy and pari-mutuel
112
+ const totalPlayers = participantsResult.rows.length;
113
+ const buyIn = parseFloat(game.buy_in);
114
+ const playerAmounts = game.player_amounts || {};
115
+
116
+ // Check if pari-mutuel (has player_amounts entries)
117
+ const isPariMutuel = Object.keys(playerAmounts).length > 0;
118
+
119
+ // Get player's bet amount (from player_amounts if pari-mutuel, else buy_in)
120
+ const getPlayerAmount = (wallet) => {
121
+ if (playerAmounts[wallet] !== undefined) {
122
+ return parseFloat(playerAmounts[wallet]);
123
+ }
124
+ return buyIn; // Legacy player
125
+ };
126
+
127
+ // Calculate actual total pot from all players
128
+ const allPlayers = [
129
+ ...(game.home_team_players || []),
130
+ ...(game.away_team_players || []),
131
+ ...(game.draw_team_players || []),
132
+ ];
133
+ const totalPot = allPlayers.reduce((sum, wallet) => sum + getPlayerAmount(wallet), 0);
134
+ // 6% total fee (5% platform + 1% oracle), 5% if user has referrer
135
+ const platformFee = totalPot * 0.06;
136
+ const winnerPool = totalPot - platformFee;
137
+
138
+ // Calculate team pools for pari-mutuel payout calculation
139
+ const homePlayers = game.home_team_players || [];
140
+ const awayPlayers = game.away_team_players || [];
141
+ const homeTeamPool = homePlayers.reduce((sum, wallet) => sum + getPlayerAmount(wallet), 0);
142
+ const awayTeamPool = awayPlayers.reduce((sum, wallet) => sum + getPlayerAmount(wallet), 0);
143
+
144
+ // Determine winners
145
+ const winningTeam = game.winner; // 'home', 'away', or 'draw'
146
+ const winners = participantsResult.rows.filter(p => p.team_choice === winningTeam);
147
+
148
+ // Calculate winner pool by team for pari-mutuel payout calculation
149
+ const winnerTeamPool = winners.reduce((sum, p) => sum + getPlayerAmount(p.wallet_address), 0);
150
+
151
+ console.log('💰 Pot Breakdown');
152
+ console.log('----------------');
153
+ console.log(`Mode: ${isPariMutuel ? 'Pari-Mutuel 🎰' : 'Legacy (equal split)'}`);
154
+ console.log(`Total Players: ${totalPlayers}`);
155
+ console.log(`Total Pot: ${totalPot.toFixed(4)} SOL`);
156
+ console.log(`Platform Fee (6%): ${platformFee.toFixed(4)} SOL`);
157
+ console.log(`Winner Pool: ${winnerPool.toFixed(4)} SOL`);
158
+
159
+ const isRefund = game.is_resolved && !winningTeam;
160
+
161
+ if (isRefund) {
162
+ // Refund scenario — show team breakdown
163
+ console.log('');
164
+ console.log(`📊 Team Pools:`);
165
+ console.log(` ${homeTeam}: ${homeTeamPool.toFixed(4)} SOL (${homePlayers.length} players)`);
166
+ console.log(` ${awayTeam}: ${awayTeamPool.toFixed(4)} SOL (${awayPlayers.length} players)`);
167
+ const refundPerPlayer = (totalPot - platformFee) / totalPlayers;
168
+ console.log(`Refund per player: ~${refundPerPlayer.toFixed(4)} SOL (after 6% fee)`);
169
+ } else if (!game.is_resolved) {
170
+ // Show team breakdown for pending games
171
+ console.log('');
172
+ console.log(`📊 Team Pools:`);
173
+ console.log(` ${homeTeam}: ${homeTeamPool.toFixed(4)} SOL (${homePlayers.length} players)`);
174
+ console.log(` ${awayTeam}: ${awayTeamPool.toFixed(4)} SOL (${awayPlayers.length} players)`);
175
+ if (isPariMutuel) {
176
+ // Show potential odds
177
+ const homeOdds = homeTeamPool > 0 ? (winnerPool / homeTeamPool).toFixed(2) : '∞';
178
+ const awayOdds = awayTeamPool > 0 ? (winnerPool / awayTeamPool).toFixed(2) : '∞';
179
+ console.log(` ${homeTeam} odds: ${homeOdds}x | ${awayTeam} odds: ${awayOdds}x`);
180
+ }
181
+ } else {
182
+ console.log(`Winners: ${winners.length}`);
183
+ if (isPariMutuel && winners.length > 0) {
184
+ console.log(`Winners' Total Bets: ${winnerTeamPool.toFixed(4)} SOL`);
185
+ console.log(`Payout: ${winnerPool.toFixed(4)} SOL split proportionally by bet size`);
186
+ } else if (winners.length > 0) {
187
+ const perWinnerAmount = winnerPool / winners.length;
188
+ console.log(`Per Winner: ${perWinnerAmount.toFixed(4)} SOL`);
189
+ }
190
+ }
191
+ console.log('');
192
+
193
+ // Display participants table
194
+ console.log('👥 Players');
195
+ console.log('----------');
196
+ console.log('');
197
+
198
+ // Table header
199
+ const header = '| Username | Team Picked | Bet | Result | Claimed | Payout |';
200
+ const divider = '|----------------|----------------|----------|----------|----------|--------------|';
201
+ console.log(header);
202
+ console.log(divider);
203
+
204
+ for (const p of participantsResult.rows) {
205
+ const username = (p.username || p.wallet_address.slice(0, 8) + '...').padEnd(14);
206
+ const teamName = p.team_choice === 'home' ? (game.home_team || 'Home') :
207
+ p.team_choice === 'away' ? (game.away_team || 'Away') : 'Draw';
208
+ const teamDisplay = teamName.slice(0, 14).padEnd(14);
209
+ const playerBet = getPlayerAmount(p.wallet_address);
210
+ const betDisplay = playerBet.toFixed(2).padStart(6) + ' ';
211
+
212
+ let result, claimStatus, payout;
213
+
214
+ if (isRefund) {
215
+ // Refund scenario
216
+ if (p.promo_code) {
217
+ // Sponsored player — refund goes to treasury, not player
218
+ result = 'PROMO 🎟️';
219
+ claimStatus = 'N/A ';
220
+ payout = ' → treasury ';
221
+ } else {
222
+ result = 'REFUND 💰';
223
+ claimStatus = p.claimed_at ? 'Yes ✅ ' : 'No ⏳ ';
224
+ const refundAmount = (totalPot - platformFee) / totalPlayers;
225
+ payout = refundAmount.toFixed(4).padStart(10) + ' SOL';
226
+ }
227
+ } else if (!game.is_resolved) {
228
+ // Game not resolved yet - calculate "if win" payout
229
+ result = 'PENDING ⏳';
230
+ claimStatus = 'N/A ';
231
+
232
+ // Calculate expected payout if this player's team wins
233
+ const playerTeamPool = p.team_choice === 'home' ? homeTeamPool : awayTeamPool;
234
+ let expectedIfWin;
235
+ if (isPariMutuel && playerTeamPool > 0) {
236
+ const share = playerBet / playerTeamPool;
237
+ expectedIfWin = winnerPool * share;
238
+ } else {
239
+ // Legacy: equal split among team members
240
+ const teamCount = p.team_choice === 'home' ? homePlayers.length : awayPlayers.length;
241
+ expectedIfWin = teamCount > 0 ? winnerPool / teamCount : 0;
242
+ }
243
+ payout = `~${expectedIfWin.toFixed(4)} SOL`;
244
+ } else {
245
+ const isWinner = p.team_choice === winningTeam;
246
+ result = isWinner ? 'WON ✅ ' : 'LOST ❌ ';
247
+
248
+ if (isWinner) {
249
+ claimStatus = p.claimed_at ? 'Yes ✅ ' : 'No ⏳ ';
250
+ // Always calculate expected payout (don't trust DB amount_claimed which may be wrong)
251
+ let expectedPayout;
252
+ if (isPariMutuel && winnerTeamPool > 0) {
253
+ // Pari-mutuel: proportional payout
254
+ const share = playerBet / winnerTeamPool;
255
+ expectedPayout = winnerPool * share;
256
+ } else {
257
+ // Legacy: equal split
258
+ expectedPayout = winnerPool / winners.length;
259
+ }
260
+ payout = expectedPayout.toFixed(4).padStart(10) + ' SOL';
261
+ } else {
262
+ claimStatus = 'N/A ';
263
+ payout = ' 0 SOL ';
264
+ }
265
+ }
266
+
267
+ console.log(`| ${username} | ${teamDisplay} | ${betDisplay} | ${result} | ${claimStatus} | ${payout} |`);
268
+ }
269
+
270
+ console.log('');
271
+
272
+ // Claim transactions section
273
+ const claimedPlayers = participantsResult.rows.filter(p => p.claim_signature);
274
+ if (claimedPlayers.length > 0) {
275
+ console.log('🔗 Claim Transactions');
276
+ console.log('---------------------');
277
+ for (const p of claimedPlayers) {
278
+ const username = p.username || p.wallet_address.slice(0, 8) + '...';
279
+ const dbAmount = p.amount_claimed ? parseFloat(p.amount_claimed) : null;
280
+ const playerBet = getPlayerAmount(p.wallet_address);
281
+
282
+ // Calculate expected payout
283
+ let expectedPayout;
284
+ if (isPariMutuel && winnerTeamPool > 0) {
285
+ const share = playerBet / winnerTeamPool;
286
+ expectedPayout = winnerPool * share;
287
+ } else {
288
+ expectedPayout = winnerPool / winners.length;
289
+ }
290
+
291
+ const explorerUrl = `https://explorer.solana.com/tx/${p.claim_signature}`;
292
+
293
+ // Check for discrepancy (>1% difference)
294
+ const hasDiscrepancy = dbAmount && Math.abs(dbAmount - expectedPayout) / expectedPayout > 0.01;
295
+
296
+ console.log(`${username}:`);
297
+ console.log(` Expected: ${expectedPayout.toFixed(4)} SOL`);
298
+ if (dbAmount) {
299
+ console.log(` DB Stored: ${dbAmount.toFixed(4)} SOL${hasDiscrepancy ? ' ⚠️ MISMATCH' : ''}`);
300
+ }
301
+ console.log(` Signature: ${p.claim_signature}`);
302
+ console.log(` Explorer: ${explorerUrl}`);
303
+ console.log('');
304
+ }
305
+ }
306
+
307
+ // Summary
308
+ console.log('📋 Summary');
309
+ console.log('----------');
310
+
311
+ if (isRefund) {
312
+ const sponsoredPlayers = participantsResult.rows.filter(p => p.promo_code);
313
+ const regularPlayers = participantsResult.rows.filter(p => !p.promo_code);
314
+ const claimedCount = regularPlayers.filter(p => p.claimed_at).length;
315
+
316
+ console.log(`💰 REFUND — ${regularPlayers.length} player(s) eligible for refund`);
317
+ if (sponsoredPlayers.length > 0) {
318
+ console.log(`🎟️ ${sponsoredPlayers.length} sponsored player(s) — refund goes to treasury`);
319
+ for (const p of sponsoredPlayers) {
320
+ console.log(` - ${p.username || p.wallet_address.slice(0, 8) + '...'} (code: ${p.promo_code})`);
321
+ }
322
+ }
323
+ console.log(`Claimed: ${claimedCount}/${regularPlayers.length}`);
324
+
325
+ const unclaimed = regularPlayers.filter(p => !p.claimed_at);
326
+ if (unclaimed.length > 0) {
327
+ console.log('');
328
+ console.log('⏳ Pending refund claims:');
329
+ for (const p of unclaimed) {
330
+ console.log(` - ${p.username || p.wallet_address.slice(0, 8) + '...'}`);
331
+ }
332
+ } else if (regularPlayers.length > 0) {
333
+ console.log('✅ All players have claimed their refund!');
334
+ }
335
+ } else if (!game.is_resolved) {
336
+ console.log('⏳ Game not yet resolved - waiting for final score');
337
+ console.log(` ${totalPlayers} players waiting for result`);
338
+ } else {
339
+ const claimedCount = winners.filter(w => w.claimed_at).length;
340
+ console.log(`Winners claimed: ${claimedCount}/${winners.length}`);
341
+
342
+ if (claimedCount < winners.length) {
343
+ console.log('');
344
+ console.log('⏳ Pending claims:');
345
+ for (const w of winners.filter(w => !w.claimed_at)) {
346
+ console.log(` - ${w.username || w.wallet_address.slice(0, 8) + '...'}`);
347
+ }
348
+ } else if (winners.length > 0) {
349
+ console.log('✅ All winners have claimed!');
350
+ }
351
+ }
352
+
353
+ console.log('');
354
+
355
+ } catch (error) {
356
+ console.error('❌ Error:', error.message);
357
+ process.exit(1);
358
+ } finally {
359
+ await pool.end();
360
+ }
361
+ }
362
+
363
+ // Main
364
+ const gameId = process.argv[2];
365
+ if (!gameId) {
366
+ console.log('Usage: node scripts/check-game-status.js <gameId>');
367
+ console.log('');
368
+ console.log('Example:');
369
+ console.log(' DATABASE_URL=$(heroku config:get DATABASE_URL -a dubs-server-prod) node scripts/check-game-status.js sport-1769329525013-mmekk2qqd');
370
+ process.exit(1);
371
+ }
372
+
373
+ checkGameStatus(gameId);
@@ -0,0 +1,270 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * List Pending Games by Version (V1 Legacy vs V2 Pari-Mutuel)
4
+ *
5
+ * Shows all pending games in production with full player details:
6
+ * - V1 (Legacy): Equal split payouts, no player_amounts
7
+ * - V2 (Pari-Mutuel): Proportional payouts based on bet amounts
8
+ *
9
+ * Usage:
10
+ * DATABASE_URL=$(heroku config:get DATABASE_URL -a dubs-server-prod) node scripts/list-pending-games-by-version.js
11
+ */
12
+
13
+ const { Pool } = require('pg');
14
+
15
+ const DATABASE_URL = process.env.DATABASE_URL;
16
+
17
+ if (!DATABASE_URL) {
18
+ console.error('❌ DATABASE_URL not set. Run with:');
19
+ console.error(' DATABASE_URL=$(heroku config:get DATABASE_URL -a dubs-server-prod) node scripts/list-pending-games-by-version.js');
20
+ process.exit(1);
21
+ }
22
+
23
+ const pool = new Pool({
24
+ connectionString: DATABASE_URL,
25
+ ssl: { rejectUnauthorized: false }
26
+ });
27
+
28
+ async function getGameDetails(game) {
29
+ const playerAmounts = game.player_amounts || {};
30
+ const buyIn = parseFloat(game.buy_in);
31
+
32
+ // Count total players
33
+ const homePlayers = game.home_team_players || [];
34
+ const awayPlayers = game.away_team_players || [];
35
+ const drawPlayers = game.draw_team_players || [];
36
+ const totalPlayerCount = homePlayers.length + awayPlayers.length + drawPlayers.length;
37
+
38
+ // V2 (pure pari-mutuel) = ALL players are in player_amounts
39
+ // V1/Hybrid = some players are NOT in player_amounts (legacy mode)
40
+ const playerAmountsCount = Object.keys(playerAmounts).length;
41
+ const isPariMutuel = totalPlayerCount > 0 && playerAmountsCount === totalPlayerCount;
42
+
43
+ const getPlayerAmount = (wallet) => {
44
+ if (playerAmounts[wallet] !== undefined) {
45
+ return parseFloat(playerAmounts[wallet]);
46
+ }
47
+ return buyIn;
48
+ };
49
+
50
+ // Get participants
51
+ const participantsResult = await pool.query(`
52
+ SELECT
53
+ ugr.wallet_address,
54
+ u.username,
55
+ ugr.team_choice
56
+ FROM user_game_refs ugr
57
+ LEFT JOIN users u ON ugr.wallet_address = u.wallet_address
58
+ WHERE ugr.game_id = $1
59
+ ORDER BY ugr.team_choice, ugr.joined_at
60
+ `, [game.game_id]);
61
+
62
+ // Calculate pools
63
+ const homeTeamPool = homePlayers.reduce((sum, wallet) => sum + getPlayerAmount(wallet), 0);
64
+ const awayTeamPool = awayPlayers.reduce((sum, wallet) => sum + getPlayerAmount(wallet), 0);
65
+ const drawTeamPool = drawPlayers.reduce((sum, wallet) => sum + getPlayerAmount(wallet), 0);
66
+ const totalPot = homeTeamPool + awayTeamPool + drawTeamPool;
67
+ const platformFee = totalPot * 0.06;
68
+ const winnerPool = totalPot - platformFee;
69
+
70
+ // Calculate odds
71
+ const homeOdds = homeTeamPool > 0 ? (winnerPool / homeTeamPool).toFixed(2) : '∞';
72
+ const awayOdds = awayTeamPool > 0 ? (winnerPool / awayTeamPool).toFixed(2) : '∞';
73
+ const drawOdds = drawTeamPool > 0 ? (winnerPool / drawTeamPool).toFixed(2) : '∞';
74
+
75
+ // Build player details
76
+ const players = participantsResult.rows.map(p => {
77
+ const playerBet = getPlayerAmount(p.wallet_address);
78
+ const playerTeamPool = p.team_choice === 'home' ? homeTeamPool :
79
+ p.team_choice === 'away' ? awayTeamPool : drawTeamPool;
80
+
81
+ let expectedIfWin;
82
+ if (isPariMutuel && playerTeamPool > 0) {
83
+ const share = playerBet / playerTeamPool;
84
+ expectedIfWin = winnerPool * share;
85
+ } else {
86
+ // Legacy: equal split among team members
87
+ const teamCount = p.team_choice === 'home' ? homePlayers.length :
88
+ p.team_choice === 'away' ? awayPlayers.length : drawPlayers.length;
89
+ expectedIfWin = teamCount > 0 ? winnerPool / teamCount : 0;
90
+ }
91
+
92
+ return {
93
+ username: p.username || p.wallet_address.slice(0, 8) + '...',
94
+ wallet: p.wallet_address,
95
+ team: p.team_choice,
96
+ teamName: p.team_choice === 'home' ? game.home_team :
97
+ p.team_choice === 'away' ? game.away_team : 'Draw',
98
+ bet: playerBet,
99
+ expectedPayout: expectedIfWin
100
+ };
101
+ });
102
+
103
+ return {
104
+ id: game.game_id,
105
+ matchup: `${game.home_team || 'Unknown'} vs ${game.away_team || 'Unknown'}`,
106
+ homeTeam: game.home_team || 'Home',
107
+ awayTeam: game.away_team || 'Away',
108
+ type: game.game_type || 'sports',
109
+ eventTime: game.event_time ? new Date(game.event_time + 'Z').toLocaleString() : 'N/A',
110
+ isPariMutuel,
111
+ buyIn,
112
+ totalPot,
113
+ winnerPool,
114
+ homeTeamPool,
115
+ awayTeamPool,
116
+ drawTeamPool,
117
+ homeOdds,
118
+ awayOdds,
119
+ drawOdds,
120
+ homePlayers: homePlayers.length,
121
+ awayPlayers: awayPlayers.length,
122
+ drawPlayers: drawPlayers.length,
123
+ players
124
+ };
125
+ }
126
+
127
+ function printGameDetails(game) {
128
+ const homeTeam = game.homeTeam || 'Home';
129
+ const awayTeam = game.awayTeam || 'Away';
130
+
131
+ console.log(` ┌─────────────────────────────────────────────────────────────────`);
132
+ console.log(` │ 🎯 ${game.id}`);
133
+ console.log(` │ ${game.matchup}`);
134
+ console.log(` │ Type: ${game.type} | Event: ${game.eventTime}`);
135
+ console.log(` │`);
136
+ console.log(` │ 💰 Pot: ${game.totalPot.toFixed(4)} SOL → Winner Pool: ${game.winnerPool.toFixed(4)} SOL (after 6% fee)`);
137
+ console.log(` │`);
138
+ console.log(` │ 📊 Team Pools:`);
139
+ console.log(` │ ${homeTeam}: ${game.homeTeamPool.toFixed(4)} SOL (${game.homePlayers} players) → ${game.homeOdds}x if win`);
140
+ console.log(` │ ${awayTeam}: ${game.awayTeamPool.toFixed(4)} SOL (${game.awayPlayers} players) → ${game.awayOdds}x if win`);
141
+ if (game.drawPlayers > 0) {
142
+ console.log(` │ Draw: ${game.drawTeamPool.toFixed(4)} SOL (${game.drawPlayers} players) → ${game.drawOdds}x if win`);
143
+ }
144
+ console.log(` │`);
145
+ console.log(` │ 👥 Players:`);
146
+ console.log(` │ ┌──────────────────┬────────────────┬──────────┬──────────────┐`);
147
+ console.log(` │ │ Username │ Team │ Bet │ If Win │`);
148
+ console.log(` │ ├──────────────────┼────────────────┼──────────┼──────────────┤`);
149
+
150
+ for (const p of game.players) {
151
+ const username = (p.username || 'Unknown').slice(0, 16).padEnd(16);
152
+ const team = (p.teamName || p.team || 'Unknown').slice(0, 14).padEnd(14);
153
+ const bet = p.bet.toFixed(4).padStart(8);
154
+ const payout = p.expectedPayout.toFixed(4).padStart(10) + ' SOL';
155
+ console.log(` │ │ ${username} │ ${team} │ ${bet} │ ${payout} │`);
156
+ }
157
+
158
+ console.log(` │ └──────────────────┴────────────────┴──────────┴──────────────┘`);
159
+ console.log(` └─────────────────────────────────────────────────────────────────`);
160
+ console.log('');
161
+ }
162
+
163
+ async function listPendingGamesByVersion() {
164
+ console.log('');
165
+ console.log('╔═══════════════════════════════════════════════════════════════════╗');
166
+ console.log('║ 🎮 PENDING GAMES BY VERSION (V1 vs V2) ║');
167
+ console.log('╚═══════════════════════════════════════════════════════════════════╝');
168
+ console.log('');
169
+
170
+ try {
171
+ // Get all pending games
172
+ const result = await pool.query(`
173
+ SELECT
174
+ g.game_id,
175
+ g.buy_in,
176
+ g.player_amounts,
177
+ g.home_team_players,
178
+ g.away_team_players,
179
+ g.draw_team_players,
180
+ g.sports_event->>'strHomeTeam' as home_team,
181
+ g.sports_event->>'strAwayTeam' as away_team,
182
+ g.sports_event->>'strTimestamp' as event_time,
183
+ g.game_type,
184
+ g.created_at
185
+ FROM games g
186
+ WHERE g.is_resolved = false
187
+ ORDER BY g.created_at DESC
188
+ `);
189
+
190
+ if (result.rows.length === 0) {
191
+ console.log('✅ No pending games found');
192
+ process.exit(0);
193
+ }
194
+
195
+ const v1Games = [];
196
+ const v2Games = [];
197
+
198
+ // Process all games
199
+ for (const game of result.rows) {
200
+ const details = await getGameDetails(game);
201
+ if (details.isPariMutuel) {
202
+ v2Games.push(details);
203
+ } else {
204
+ v1Games.push(details);
205
+ }
206
+ }
207
+
208
+ // Display V1 (Legacy/Hybrid) games
209
+ console.log('┌───────────────────────────────────────────────────────────────────┐');
210
+ console.log('│ 📦 V1 (LEGACY/HYBRID) - Equal Split Payouts │');
211
+ console.log('│ Detection: player_amounts.length != total_players │');
212
+ console.log('└───────────────────────────────────────────────────────────────────┘');
213
+ console.log('');
214
+
215
+ if (v1Games.length === 0) {
216
+ console.log(' (none)');
217
+ console.log('');
218
+ } else {
219
+ console.log(` Total: ${v1Games.length} games`);
220
+ console.log('');
221
+ for (const game of v1Games) {
222
+ printGameDetails(game);
223
+ }
224
+ }
225
+
226
+ // Display V2 (Pari-Mutuel) games
227
+ console.log('┌───────────────────────────────────────────────────────────────────┐');
228
+ console.log('│ 🎰 V2 (PARI-MUTUEL) - Proportional Payouts │');
229
+ console.log('│ Detection: player_amounts has entries │');
230
+ console.log('└───────────────────────────────────────────────────────────────────┘');
231
+ console.log('');
232
+
233
+ if (v2Games.length === 0) {
234
+ console.log(' (none)');
235
+ console.log('');
236
+ } else {
237
+ console.log(` Total: ${v2Games.length} games`);
238
+ console.log('');
239
+ for (const game of v2Games) {
240
+ printGameDetails(game);
241
+ }
242
+ }
243
+
244
+ // Summary
245
+ console.log('╔═══════════════════════════════════════════════════════════════════╗');
246
+ console.log('║ 📊 SUMMARY ║');
247
+ console.log('╠═══════════════════════════════════════════════════════════════════╣');
248
+
249
+ const v1Pot = v1Games.reduce((sum, g) => sum + g.totalPot, 0);
250
+ const v2Pot = v2Games.reduce((sum, g) => sum + g.totalPot, 0);
251
+ const v1Players = v1Games.reduce((sum, g) => sum + g.players.length, 0);
252
+ const v2Players = v2Games.reduce((sum, g) => sum + g.players.length, 0);
253
+
254
+ console.log(`║ V1 (Legacy/Hybrid): ${String(v1Games.length).padStart(3)} games | ${String(v1Players).padStart(4)} players | ${v1Pot.toFixed(4).padStart(10)} SOL ║`);
255
+ console.log(`║ V2 (Pari-Mutuel): ${String(v2Games.length).padStart(3)} games | ${String(v2Players).padStart(4)} players | ${v2Pot.toFixed(4).padStart(10)} SOL ║`);
256
+ console.log(`║ ───────────────────────────────────────────────────────────── ║`);
257
+ console.log(`║ TOTAL: ${String(result.rows.length).padStart(3)} games | ${String(v1Players + v2Players).padStart(4)} players | ${(v1Pot + v2Pot).toFixed(4).padStart(10)} SOL ║`);
258
+ console.log('╚═══════════════════════════════════════════════════════════════════╝');
259
+ console.log('');
260
+
261
+ } catch (error) {
262
+ console.error('❌ Error:', error.message);
263
+ console.error(error.stack);
264
+ process.exit(1);
265
+ } finally {
266
+ await pool.end();
267
+ }
268
+ }
269
+
270
+ listPendingGamesByVersion();