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,440 @@
1
+ /**
2
+ * Pick'em Oracle
3
+ * Polls UFC fight results via @dubsdotapp/node SDK, resolves pools, distributes winnings on-chain.
4
+ * Pattern mirrors survivorOracle.js
5
+ */
6
+
7
+ const { Dubs } = require('@dubsdotapp/node');
8
+ const { Connection, Keypair, PublicKey, Transaction, TransactionInstruction } = require('@solana/web3.js');
9
+ const crypto = require('crypto');
10
+ const { pool } = require('./db');
11
+ const pickemController = require('../controllers/pickemController');
12
+
13
+ const LAMPORTS_PER_SOL = 1_000_000_000;
14
+ const OPERATOR_WALLET = new PublicKey('BVZXwZpfgyzTBdRFHohkHZppPHnAyqyctRsKy3vWfQib');
15
+
16
+ // distribute_survivor_winnings discriminator (sha256("global:distribute_survivor_winnings")[0..8])
17
+ const DIST_SURVIVOR = Buffer.from(
18
+ crypto.createHash('sha256').update('global:distribute_survivor_winnings').digest().slice(0, 8)
19
+ );
20
+
21
+ class PickemOracle {
22
+ constructor(config = {}) {
23
+ this.checkIntervalMs = config.checkIntervalMs || 5 * 60 * 1000; // 5 minutes
24
+ this.isRunning = false;
25
+ this.intervalId = null;
26
+
27
+ // Solana config
28
+ this.rpcUrl = config.rpcUrl || process.env.SOLANA_RPC_URL || 'https://api.devnet.solana.com';
29
+ this.programId = new PublicKey(config.programId || process.env.PROGRAM_ID || '85wJGp9uc8w2FeKX9CEHsudTo1UVCrmuRFy37oCcaoG1');
30
+ this.connection = new Connection(this.rpcUrl, 'confirmed');
31
+ this.oracleKeypair = config.oracleKeypair || null;
32
+
33
+ // Dubs SDK client — dogfooding our own API
34
+ this.dubs = new Dubs({
35
+ apiKey: config.apiKey || process.env.DUBS_API_KEY || '',
36
+ baseUrl: config.baseUrl || process.env.DUBS_SDK_BASE_URL || `http://localhost:${process.env.PORT || 3001}/api/developer/v1`,
37
+ });
38
+
39
+ // Socket.IO ref (injected externally if available)
40
+ this.io = config.io || null;
41
+ }
42
+
43
+ start() {
44
+ if (this.isRunning) return;
45
+ this.isRunning = true;
46
+
47
+ console.log('🥊 Starting Pick\'em Oracle...');
48
+ console.log(` Check interval: ${this.checkIntervalMs / 1000}s`);
49
+ console.log(` RPC: ${this.rpcUrl}`);
50
+ console.log(` Program: ${this.programId.toString()}`);
51
+ if (this.oracleKeypair) {
52
+ console.log(` Oracle wallet: ${this.oracleKeypair.publicKey.toString()}`);
53
+ }
54
+
55
+ // Run immediately, then on interval
56
+ this.checkPools().catch(err => console.error('[PickemOracle] Initial check error:', err.message));
57
+ this.intervalId = setInterval(() => {
58
+ this.checkPools().catch(err => console.error('[PickemOracle] Check error:', err.message));
59
+ }, this.checkIntervalMs);
60
+ }
61
+
62
+ stop() {
63
+ if (!this.isRunning) return;
64
+ this.isRunning = false;
65
+ if (this.intervalId) {
66
+ clearInterval(this.intervalId);
67
+ this.intervalId = null;
68
+ }
69
+ console.log('🥊 Pick\'em Oracle stopped.');
70
+ }
71
+
72
+ // ========== MAIN POLLING LOOP ==========
73
+
74
+ async checkPools() {
75
+ console.log('[PickemOracle] Checking pools...');
76
+
77
+ // 1. Auto-lock pools past lock_time
78
+ await this.autoLockPools();
79
+
80
+ // 2. Get active pools (locked or resolving)
81
+ const result = await pool.query(
82
+ "SELECT * FROM pickem_pools WHERE status IN ('locked', 'resolving')"
83
+ );
84
+ const activePools = result.rows;
85
+
86
+ if (activePools.length === 0) {
87
+ console.log('[PickemOracle] No active pools to process.');
88
+ return;
89
+ }
90
+
91
+ console.log(`[PickemOracle] Processing ${activePools.length} active pool(s)...`);
92
+
93
+ // 3. Fetch fight card data via SDK
94
+ let fightCardData;
95
+ try {
96
+ fightCardData = await this.dubs.ufc.fightCard();
97
+ } catch (err) {
98
+ console.error('[PickemOracle] Failed to fetch UFC fight card via SDK:', err.message);
99
+ return;
100
+ }
101
+
102
+ // Build a lookup: competitionId → fight result
103
+ const fightResultMap = new Map();
104
+ if (fightCardData && fightCardData.events) {
105
+ for (const event of fightCardData.events) {
106
+ for (const fight of event.fights) {
107
+ if (fight.id) {
108
+ fightResultMap.set(fight.id, fight);
109
+ }
110
+ }
111
+ }
112
+ }
113
+
114
+ console.log(`[PickemOracle] UFC fight data: ${fightResultMap.size} fights tracked`);
115
+
116
+ // 4. Process each pool
117
+ for (const p of activePools) {
118
+ try {
119
+ await this.processPool(p, fightResultMap);
120
+ } catch (err) {
121
+ console.error(`[PickemOracle] Error processing pool ${p.id}:`, err.message);
122
+ }
123
+ }
124
+ }
125
+
126
+ // ========== AUTO-LOCK ==========
127
+
128
+ async autoLockPools() {
129
+ const result = await pool.query(
130
+ "UPDATE pickem_pools SET status = 'locked', updated_at = NOW() WHERE status = 'open' AND lock_time <= NOW() RETURNING id, name"
131
+ );
132
+ for (const row of result.rows) {
133
+ console.log(`[PickemOracle] Auto-locked pool ${row.id}: ${row.name}`);
134
+ if (this.io) {
135
+ this.io.emit('pickem:pool_locked', { poolId: row.id });
136
+ }
137
+ }
138
+ }
139
+
140
+ // ========== PROCESS POOL ==========
141
+
142
+ async processPool(pickemPool, fightResultMap) {
143
+ // Get unresolved fights for this pool
144
+ const fightsResult = await pool.query(
145
+ "SELECT * FROM pickem_fights WHERE pool_id = $1 AND status NOT IN ('final', 'cancelled', 'no_contest') ORDER BY fight_order",
146
+ [pickemPool.id]
147
+ );
148
+ const unresolvedFights = fightsResult.rows;
149
+
150
+ if (unresolvedFights.length === 0) {
151
+ // All fights resolved — trigger full pool resolution
152
+ console.log(`[PickemOracle] Pool ${pickemPool.id}: All fights resolved, triggering pool resolution...`);
153
+ await this.resolvePool(pickemPool.id);
154
+ return;
155
+ }
156
+
157
+ // Update each unresolved fight from SDK data
158
+ let newlyResolved = 0;
159
+ for (const fight of unresolvedFights) {
160
+ const sdkFight = fightResultMap.get(fight.espn_competition_id);
161
+ if (!sdkFight) continue;
162
+
163
+ if (sdkFight.status === 'Final') {
164
+ // Determine winner: 'a' = home/first, 'b' = away/second
165
+ let winner = null;
166
+ if (sdkFight.home?.winner === true) {
167
+ winner = 'a';
168
+ } else if (sdkFight.away?.winner === true) {
169
+ winner = 'b';
170
+ } else if (sdkFight.home?.score > sdkFight.away?.score) {
171
+ winner = 'a';
172
+ } else if (sdkFight.away?.score > sdkFight.home?.score) {
173
+ winner = 'b';
174
+ }
175
+
176
+ const method = sdkFight.ufcData?.statusDetail || sdkFight.statusDetail || null;
177
+
178
+ if (!winner) {
179
+ console.log(`[PickemOracle] Pool ${pickemPool.id} Fight #${fight.fight_order}: ${fight.fighter_a_name} vs ${fight.fighter_b_name} → FINAL but no winner detected! SDK data: home.winner=${sdkFight.home?.winner}, away.winner=${sdkFight.away?.winner}, home.score=${sdkFight.home?.score}, away.score=${sdkFight.away?.score}`);
180
+ }
181
+
182
+ await pickemController.updateFightResult(fight.id, {
183
+ winner,
184
+ method,
185
+ status: 'final',
186
+ });
187
+
188
+ const winnerName = winner === 'a' ? fight.fighter_a_name : winner === 'b' ? fight.fighter_b_name : 'UNKNOWN';
189
+ console.log(`[PickemOracle] Pool ${pickemPool.id} Fight #${fight.fight_order}: ${fight.fighter_a_name} vs ${fight.fighter_b_name} → Winner: ${winnerName} (${method || 'unknown'})`);
190
+
191
+ if (this.io) {
192
+ this.io.emit('pickem:fight_resolved', {
193
+ poolId: pickemPool.id,
194
+ fightId: fight.id,
195
+ winner,
196
+ method,
197
+ });
198
+ }
199
+ newlyResolved++;
200
+ } else if (['Canceled', 'Postponed', 'Cancelled'].includes(sdkFight.status)) {
201
+ await pickemController.updateFightResult(fight.id, {
202
+ winner: null,
203
+ method: null,
204
+ status: 'cancelled',
205
+ });
206
+ console.log(`[PickemOracle] Pool ${pickemPool.id} Fight #${fight.fight_order}: ${fight.fighter_a_name} vs ${fight.fighter_b_name} → Cancelled`);
207
+ newlyResolved++;
208
+ } else if (sdkFight.status === 'In Progress') {
209
+ await pool.query(
210
+ "UPDATE pickem_fights SET status = 'live', updated_at = NOW() WHERE id = $1",
211
+ [fight.id]
212
+ );
213
+ }
214
+ }
215
+
216
+ if (newlyResolved > 0) {
217
+ console.log(`[PickemOracle] Pool ${pickemPool.id}: ${newlyResolved} fight(s) newly resolved`);
218
+
219
+ // Incrementally mark picks correct/incorrect and recompute scores
220
+ try {
221
+ // Mark is_correct for all resolved fights in this pool
222
+ await pool.query(`
223
+ UPDATE pickem_picks pp
224
+ SET is_correct = (pp.pick = f.winner), updated_at = NOW()
225
+ FROM pickem_fights f
226
+ WHERE pp.fight_id = f.id
227
+ AND f.pool_id = $1
228
+ AND f.status = 'final'
229
+ AND f.winner IS NOT NULL
230
+ AND pp.is_correct IS NULL
231
+ `, [pickemPool.id]);
232
+
233
+ // Recompute scores for all entries in this pool
234
+ await pool.query(`
235
+ UPDATE pickem_entries e
236
+ SET score = (SELECT COUNT(*) FROM pickem_picks p WHERE p.entry_id = e.id AND p.is_correct = true),
237
+ updated_at = NOW()
238
+ WHERE e.pool_id = $1
239
+ `, [pickemPool.id]);
240
+
241
+ console.log(`[PickemOracle] Pool ${pickemPool.id}: Scores updated incrementally`);
242
+ } catch (scoreErr) {
243
+ console.error(`[PickemOracle] Pool ${pickemPool.id}: Error updating incremental scores:`, scoreErr.message);
244
+ }
245
+
246
+ // Check if all fights are now terminal
247
+ const stats = await pickemController.getPoolStats(pickemPool.id);
248
+ if (stats.allFightsResolved) {
249
+ console.log(`[PickemOracle] Pool ${pickemPool.id}: All fights now resolved, triggering pool resolution...`);
250
+ await this.resolvePool(pickemPool.id);
251
+ }
252
+ }
253
+ }
254
+
255
+ // ========== RESOLVE POOL ==========
256
+
257
+ async resolvePool(poolId) {
258
+ const client = await pool.connect();
259
+ try {
260
+ await client.query('BEGIN');
261
+
262
+ // Lock pool row
263
+ const poolResult = await client.query(
264
+ "SELECT * FROM pickem_pools WHERE id = $1 FOR UPDATE",
265
+ [poolId]
266
+ );
267
+ const pickemPool = poolResult.rows[0];
268
+ if (!pickemPool) throw new Error(`Pool ${poolId} not found`);
269
+ if (pickemPool.status === 'complete') {
270
+ console.log(`[PickemOracle] Pool ${poolId} already complete, skipping.`);
271
+ await client.query('COMMIT');
272
+ return;
273
+ }
274
+
275
+ // Set status to resolving
276
+ await client.query(
277
+ "UPDATE pickem_pools SET status = 'resolving', updated_at = NOW() WHERE id = $1",
278
+ [poolId]
279
+ );
280
+
281
+ await client.query('COMMIT');
282
+ } catch (err) {
283
+ await client.query('ROLLBACK');
284
+ throw err;
285
+ } finally {
286
+ client.release();
287
+ }
288
+
289
+ // Compute scores (uses its own transaction)
290
+ console.log(`[PickemOracle] Pool ${poolId}: Computing scores...`);
291
+ const scoreResult = await pickemController.computeScores(poolId);
292
+ console.log(`[PickemOracle] Pool ${poolId}: Max score = ${scoreResult.maxScore}, Winners = ${scoreResult.winnerCount}`);
293
+
294
+ // Distribute on-chain if we have a Solana game
295
+ const poolResult = await pool.query('SELECT * FROM pickem_pools WHERE id = $1', [poolId]);
296
+ const pickemPool = poolResult.rows[0];
297
+
298
+ if (pickemPool.solana_game_id && this.oracleKeypair && scoreResult.winners.length > 0) {
299
+ try {
300
+ console.log(`[PickemOracle] Pool ${poolId}: Distributing winnings on-chain...`);
301
+ const signatures = await this.distributeWinnings(pickemPool, scoreResult.winners);
302
+
303
+ // Record payouts
304
+ const totalEntries = Number(pickemPool.total_entries);
305
+ const totalPotLamports = totalEntries * Number(pickemPool.buy_in_lamports);
306
+ const netPotLamports = Math.floor(totalPotLamports * 0.94); // 6% fee
307
+ const perWinner = Math.floor(netPotLamports / scoreResult.winnerCount);
308
+
309
+ for (const winner of scoreResult.winners) {
310
+ await pickemController.recordPayout({
311
+ poolId,
312
+ entryId: winner.id,
313
+ walletAddress: winner.walletAddress,
314
+ amountLamports: perWinner,
315
+ txSignature: signatures[0], // First batch signature
316
+ status: 'completed',
317
+ });
318
+ }
319
+
320
+ console.log(`[PickemOracle] Pool ${poolId}: Payouts recorded. ${scoreResult.winnerCount} winner(s), ${perWinner / LAMPORTS_PER_SOL} SOL each.`);
321
+ } catch (err) {
322
+ console.error(`[PickemOracle] Pool ${poolId}: On-chain distribution failed:`, err.message);
323
+ // Mark payouts as failed but still complete the pool
324
+ for (const winner of scoreResult.winners) {
325
+ await pickemController.recordPayout({
326
+ poolId,
327
+ entryId: winner.id,
328
+ walletAddress: winner.walletAddress,
329
+ amountLamports: 0,
330
+ txSignature: null,
331
+ status: 'failed',
332
+ });
333
+ }
334
+ }
335
+ } else {
336
+ if (!pickemPool.solana_game_id) console.log(`[PickemOracle] Pool ${poolId}: No solana_game_id, skipping on-chain distribution.`);
337
+ if (!this.oracleKeypair) console.log(`[PickemOracle] Pool ${poolId}: No oracle keypair, skipping on-chain distribution.`);
338
+ if (scoreResult.winners.length === 0) console.log(`[PickemOracle] Pool ${poolId}: No entries, skipping distribution.`);
339
+ }
340
+
341
+ // Mark pool complete
342
+ await pool.query(
343
+ "UPDATE pickem_pools SET status = 'complete', updated_at = NOW() WHERE id = $1",
344
+ [poolId]
345
+ );
346
+
347
+ console.log(`[PickemOracle] Pool ${poolId}: COMPLETE ✅`);
348
+
349
+ if (this.io) {
350
+ this.io.emit('pickem:pool_resolved', {
351
+ poolId,
352
+ maxScore: scoreResult.maxScore,
353
+ winnerCount: scoreResult.winnerCount,
354
+ winners: scoreResult.winners.map(w => ({
355
+ walletAddress: w.walletAddress,
356
+ username: w.username,
357
+ score: w.score,
358
+ })),
359
+ });
360
+ }
361
+ }
362
+
363
+ // ========== SOLANA DISTRIBUTION ==========
364
+
365
+ async distributeWinnings(pickemPool, winners) {
366
+ const gameIdNum = BigInt(pickemPool.solana_game_id);
367
+ const gameIdBuf = Buffer.alloc(8);
368
+ gameIdBuf.writeBigUInt64LE(gameIdNum);
369
+
370
+ const [gamePDA] = PublicKey.findProgramAddressSync(
371
+ [Buffer.from('game'), gameIdBuf],
372
+ this.programId
373
+ );
374
+
375
+ // Batch winners (max 50 per tx due to contract limit)
376
+ const batches = [];
377
+ for (let i = 0; i < winners.length; i += 50) {
378
+ batches.push(winners.slice(i, i + 50));
379
+ }
380
+
381
+ const signatures = [];
382
+ for (let batchIdx = 0; batchIdx < batches.length; batchIdx++) {
383
+ const batch = batches[batchIdx];
384
+
385
+ const data = Buffer.concat([DIST_SURVIVOR, gameIdBuf]);
386
+
387
+ const keys = [
388
+ { pubkey: gamePDA, isSigner: false, isWritable: true },
389
+ { pubkey: this.oracleKeypair.publicKey, isSigner: true, isWritable: true },
390
+ // remaining_accounts[0] = operator wallet
391
+ { pubkey: OPERATOR_WALLET, isSigner: false, isWritable: true },
392
+ ];
393
+
394
+ // remaining_accounts[1..n] = winner wallets
395
+ for (const w of batch) {
396
+ keys.push({ pubkey: new PublicKey(w.walletAddress), isSigner: false, isWritable: true });
397
+ }
398
+
399
+ const ix = new TransactionInstruction({
400
+ keys,
401
+ programId: this.programId,
402
+ data,
403
+ });
404
+
405
+ const tx = new Transaction().add(ix);
406
+ const { blockhash } = await this.connection.getLatestBlockhash();
407
+ tx.recentBlockhash = blockhash;
408
+ tx.feePayer = this.oracleKeypair.publicKey;
409
+ tx.sign(this.oracleKeypair);
410
+
411
+ console.log(`[PickemOracle] Sending distribute_survivor_winnings batch ${batchIdx + 1}/${batches.length} (${batch.length} winners)...`);
412
+ const sig = await this.connection.sendRawTransaction(tx.serialize());
413
+ await this.confirmTransactionPolling(sig);
414
+ signatures.push(sig);
415
+ console.log(`[PickemOracle] Batch ${batchIdx + 1} confirmed: ${sig}`);
416
+ }
417
+
418
+ return signatures;
419
+ }
420
+
421
+ // ========== HELPERS ==========
422
+
423
+ async confirmTransactionPolling(signature, timeout = 60000) {
424
+ const start = Date.now();
425
+ while (Date.now() - start < timeout) {
426
+ const statuses = await this.connection.getSignatureStatuses([signature]);
427
+ const status = statuses?.value?.[0];
428
+ if (status?.err) {
429
+ throw new Error(`Transaction failed: ${JSON.stringify(status.err)}`);
430
+ }
431
+ if (status?.confirmationStatus === 'confirmed' || status?.confirmationStatus === 'finalized') {
432
+ return status;
433
+ }
434
+ await new Promise(resolve => setTimeout(resolve, 1000));
435
+ }
436
+ throw new Error('Transaction confirmation timeout');
437
+ }
438
+ }
439
+
440
+ module.exports = PickemOracle;