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,260 @@
1
+ /**
2
+ * Custom Game Resolver Service
3
+ *
4
+ * Handles on-chain resolution of developer custom games (game_mode=6).
5
+ * Extracted from AutomaticGameOracle to avoid circular dependencies
6
+ * with the polling loop / ESPN / PandaScore integration.
7
+ *
8
+ * Shares the same oracle keypair, program ID, and transaction format.
9
+ */
10
+
11
+ const { Connection, Keypair, PublicKey, Transaction, TransactionInstruction } = require('@solana/web3.js');
12
+ const crypto = require('crypto');
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const axios = require('axios');
16
+
17
+ // Must match the contract's hardcoded address
18
+ const OPERATOR_WALLET = new PublicKey('BVZXwZpfgyzTBdRFHohkHZppPHnAyqyctRsKy3vWfQib');
19
+
20
+ // resolve_automatic_game discriminator
21
+ const RESOLVE_AUTO = Buffer.from([245, 33, 115, 150, 82, 150, 28, 193]);
22
+
23
+ // authority_force_lock discriminator — sha256("global:authority_force_lock")[0..8]
24
+ // Sets lock_timestamp = now so resolve_automatic_game passes the lock check
25
+ const AUTHORITY_FORCE_LOCK = Buffer.from([186, 206, 107, 11, 185, 154, 217, 166]);
26
+
27
+ class CustomGameResolver {
28
+ constructor() {
29
+ const rpcUrl = process.env.SOLANA_NETWORK || 'https://api.devnet.solana.com';
30
+ const programId = process.env.PROGRAM_ID || '85wJGp9uc8w2FeKX9CEHsudTo1UVCrmuRFy37oCcaoG1';
31
+
32
+ this.connection = new Connection(rpcUrl, 'confirmed');
33
+ this.programId = new PublicKey(programId);
34
+ this.oracleKeypair = this._loadOracleKeypair();
35
+ this.dubsServerUrl = process.env.DUBS_SERVER_URL || `http://localhost:${process.env.PORT || 3001}`;
36
+ }
37
+
38
+ /**
39
+ * Load oracle keypair from env var or wallet file.
40
+ * Returns null if unavailable (resolve calls will fail gracefully).
41
+ */
42
+ _loadOracleKeypair() {
43
+ try {
44
+ if (process.env.ORACLE_WALLET_JSON) {
45
+ const secretKey = JSON.parse(process.env.ORACLE_WALLET_JSON);
46
+ return Keypair.fromSecretKey(Uint8Array.from(secretKey));
47
+ }
48
+
49
+ const walletPath = process.env.ORACLE_WALLET_PATH || path.join(__dirname, '../wallets/oracle.json');
50
+ if (fs.existsSync(walletPath)) {
51
+ const secretKey = JSON.parse(fs.readFileSync(walletPath, 'utf-8'));
52
+ return Keypair.fromSecretKey(Uint8Array.from(secretKey));
53
+ }
54
+
55
+ console.warn('[CustomGameResolver] No oracle keypair found — resolve will be disabled');
56
+ return null;
57
+ } catch (err) {
58
+ console.error('[CustomGameResolver] Failed to load oracle keypair:', err.message);
59
+ return null;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Resolve a custom game on-chain and update the database.
65
+ *
66
+ * @param {string} gameId - The game ID
67
+ * @param {string|null} winner - 'home', 'away', 'draw', or null (refund)
68
+ * @param {object} [metadata] - Optional proof/metadata from developer
69
+ * @returns {{ signature: string }} - On-chain transaction signature
70
+ */
71
+ async resolveGame(gameId, winner, metadata = null) {
72
+ if (!this.oracleKeypair) {
73
+ throw new Error('Oracle keypair not loaded — cannot resolve games');
74
+ }
75
+
76
+ console.log(`\n[CustomGameResolver] Resolving game: ${gameId}`);
77
+ console.log(` Winner: ${winner === null ? 'null (REFUND)' : winner}`);
78
+
79
+ // Look up referrer for commission payout
80
+ const referrerWallet = await this.getGameCreatorReferrer(gameId);
81
+ if (referrerWallet) {
82
+ console.log(` Referrer: ${referrerWallet}`);
83
+ }
84
+
85
+ // Build + send on-chain resolve transaction
86
+ const signature = await this.buildAndSendResolveTransaction(gameId, winner, referrerWallet);
87
+ console.log(` On-chain signature: ${signature}`);
88
+
89
+ // Update PostgreSQL
90
+ await this.updateGameInPostgreSQL(gameId, winner, signature);
91
+
92
+ console.log(`[CustomGameResolver] Game ${gameId} resolved successfully`);
93
+ return { signature };
94
+ }
95
+
96
+ /**
97
+ * Build and send authority_force_lock + resolve_automatic_game in a single transaction.
98
+ *
99
+ * The force-lock instruction sets lock_timestamp = now so the resolve instruction's
100
+ * `current_time >= lock_timestamp` check passes immediately. This lets developers
101
+ * resolve custom games at any time without waiting for an artificial lock timer.
102
+ */
103
+ async buildAndSendResolveTransaction(gameId, winner, referrerWallet = null) {
104
+ // Derive game PDA
105
+ let gameIdNum;
106
+ if (typeof gameId === 'string' && gameId.includes('-')) {
107
+ const hash = crypto.createHash('sha256').update(gameId).digest();
108
+ gameIdNum = hash.readBigUInt64LE(0);
109
+ } else {
110
+ gameIdNum = BigInt(gameId);
111
+ }
112
+
113
+ const gameIdBuf = Buffer.alloc(8);
114
+ gameIdBuf.writeBigUInt64LE(gameIdNum);
115
+
116
+ const [gamePDA] = PublicKey.findProgramAddressSync(
117
+ [Buffer.from('game'), gameIdBuf],
118
+ this.programId
119
+ );
120
+
121
+ // Instruction 1: authority_force_lock — sets lock_timestamp = now
122
+ const forceLockData = Buffer.concat([AUTHORITY_FORCE_LOCK, gameIdBuf]);
123
+ const forceLockIx = new TransactionInstruction({
124
+ keys: [
125
+ { pubkey: gamePDA, isSigner: false, isWritable: true },
126
+ { pubkey: this.oracleKeypair.publicKey, isSigner: true, isWritable: true },
127
+ ],
128
+ programId: this.programId,
129
+ data: forceLockData,
130
+ });
131
+
132
+ // Instruction 2: resolve_automatic_game
133
+ // Encode winning team: Some(Home)=[1,0], Some(Away)=[1,1], Some(Draw)=[1,2], None(refund)=[0]
134
+ let winningTeamBytes;
135
+ if (winner === null) {
136
+ winningTeamBytes = Buffer.from([0]);
137
+ } else if (winner === 'home') {
138
+ winningTeamBytes = Buffer.from([1, 0]);
139
+ } else if (winner === 'away') {
140
+ winningTeamBytes = Buffer.from([1, 1]);
141
+ } else if (winner === 'draw') {
142
+ winningTeamBytes = Buffer.from([1, 2]);
143
+ } else {
144
+ console.error(`[CustomGameResolver] Unexpected winner value: ${winner}, defaulting to refund`);
145
+ winningTeamBytes = Buffer.from([0]);
146
+ }
147
+
148
+ const resolveData = Buffer.concat([RESOLVE_AUTO, gameIdBuf, winningTeamBytes]);
149
+
150
+ const resolveKeys = [
151
+ { pubkey: gamePDA, isSigner: false, isWritable: true },
152
+ { pubkey: this.oracleKeypair.publicKey, isSigner: true, isWritable: true },
153
+ { pubkey: OPERATOR_WALLET, isSigner: false, isWritable: true },
154
+ ];
155
+
156
+ if (referrerWallet) {
157
+ try {
158
+ const referrerPubkey = new PublicKey(referrerWallet);
159
+ resolveKeys.push({ pubkey: referrerPubkey, isSigner: false, isWritable: true });
160
+ } catch (e) {
161
+ console.warn(`[CustomGameResolver] Invalid referrer wallet: ${referrerWallet}`);
162
+ }
163
+ }
164
+
165
+ const resolveIx = new TransactionInstruction({
166
+ keys: resolveKeys,
167
+ programId: this.programId,
168
+ data: resolveData,
169
+ });
170
+
171
+ // Combine both instructions: force-lock first, then resolve
172
+ const tx = new Transaction().add(forceLockIx).add(resolveIx);
173
+ tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash;
174
+ tx.feePayer = this.oracleKeypair.publicKey;
175
+ tx.sign(this.oracleKeypair);
176
+
177
+ const signature = await this.connection.sendRawTransaction(tx.serialize());
178
+ await this.confirmTransactionPolling(signature);
179
+
180
+ return signature;
181
+ }
182
+
183
+ /**
184
+ * Update game status in PostgreSQL via the internal resolve endpoint.
185
+ */
186
+ async updateGameInPostgreSQL(gameId, winner, signature) {
187
+ try {
188
+ await axios.post(
189
+ `${this.dubsServerUrl}/api/games/${gameId}/resolve`,
190
+ {
191
+ winner,
192
+ homeScore: null,
193
+ awayScore: null,
194
+ resolvedAt: new Date().toISOString(),
195
+ resolvedBy: 'developer',
196
+ resolveSignature: signature,
197
+ },
198
+ { timeout: 10000, headers: { 'Content-Type': 'application/json' } }
199
+ );
200
+ console.log(` PostgreSQL updated`);
201
+ } catch (error) {
202
+ console.error(`[CustomGameResolver] Error updating PostgreSQL:`, error.message);
203
+ // Don't throw — on-chain resolution is what matters
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Look up the game creator's referrer wallet for commission payout.
209
+ */
210
+ async getGameCreatorReferrer(gameId) {
211
+ try {
212
+ const response = await axios.get(
213
+ `${this.dubsServerUrl}/api/games/${gameId}/creator-referrer`,
214
+ { timeout: 5000 }
215
+ );
216
+ if (response.data.success && response.data.referrerWallet) {
217
+ return response.data.referrerWallet;
218
+ }
219
+ return null;
220
+ } catch (error) {
221
+ console.warn(`[CustomGameResolver] Could not fetch referrer for ${gameId}:`, error.message);
222
+ return null;
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Poll for transaction confirmation.
228
+ */
229
+ async confirmTransactionPolling(signature, timeout = 60000) {
230
+ const start = Date.now();
231
+ while (Date.now() - start < timeout) {
232
+ const statuses = await this.connection.getSignatureStatuses([signature]);
233
+ const status = statuses?.value?.[0];
234
+
235
+ if (status?.err) {
236
+ throw new Error(`Transaction failed: ${JSON.stringify(status.err)}`);
237
+ }
238
+
239
+ if (status?.confirmationStatus === 'confirmed' || status?.confirmationStatus === 'finalized') {
240
+ console.log(` Transaction confirmed: ${status.confirmationStatus}`);
241
+ return status;
242
+ }
243
+
244
+ await new Promise(resolve => setTimeout(resolve, 1000));
245
+ }
246
+ throw new Error('Transaction confirmation timeout');
247
+ }
248
+ }
249
+
250
+ // Singleton
251
+ let instance = null;
252
+
253
+ function getCustomGameResolver() {
254
+ if (!instance) {
255
+ instance = new CustomGameResolver();
256
+ }
257
+ return instance;
258
+ }
259
+
260
+ module.exports = { CustomGameResolver, getCustomGameResolver };
package/services/db.js ADDED
@@ -0,0 +1,79 @@
1
+ /**
2
+ * 🗄️ Shared Database Pool
3
+ *
4
+ * Centralized PostgreSQL connection pool for the entire application.
5
+ * IMPORTANT: All services should import from here instead of creating their own pools.
6
+ *
7
+ * Heroku PostgreSQL connection limits:
8
+ * - Hobby/Basic: 20 connections
9
+ * - Standard-0: 120 connections
10
+ * - Standard-2: 400 connections
11
+ *
12
+ * We set max to 8 to leave room for:
13
+ * - Heroku's own management connections
14
+ * - Multiple dynos (if scaled)
15
+ * - External tools (pgAdmin, migrations, etc.)
16
+ */
17
+
18
+ const { Pool } = require('pg');
19
+
20
+ // Determine if we're on Heroku/AWS (need SSL)
21
+ const isProduction = process.env.DATABASE_URL &&
22
+ (process.env.DATABASE_URL.includes('amazonaws') || process.env.DATABASE_URL.includes('heroku'));
23
+
24
+ // Create the shared pool with explicit connection limits
25
+ const pool = process.env.DATABASE_URL ? new Pool({
26
+ connectionString: process.env.DATABASE_URL,
27
+ ssl: isProduction ? { rejectUnauthorized: false } : false,
28
+
29
+ // Connection pool settings - CRITICAL for Heroku
30
+ max: parseInt(process.env.DB_POOL_MAX, 10) || 5, // Maximum connections in pool (reduced from 8)
31
+ min: 0, // Don't hold idle connections unnecessarily
32
+ idleTimeoutMillis: 10000, // Close idle connections after 10s (was 30s)
33
+ connectionTimeoutMillis: 10000, // Timeout acquiring connection after 10s
34
+ allowExitOnIdle: true, // Allow process to exit when pool is idle
35
+ }) : null;
36
+
37
+ // Log pool configuration on startup
38
+ if (pool) {
39
+ console.log('[DB] Shared pool created:', {
40
+ max: pool.options.max,
41
+ min: pool.options.min,
42
+ idleTimeoutMillis: pool.options.idleTimeoutMillis,
43
+ ssl: !!pool.options.ssl
44
+ });
45
+
46
+ // Monitor pool events for debugging connection issues
47
+ pool.on('error', (err) => {
48
+ console.error('[DB] Unexpected pool error:', err.message);
49
+ });
50
+
51
+ pool.on('connect', () => {
52
+ console.log('[DB] New client connected. Total:', pool.totalCount, 'Idle:', pool.idleCount);
53
+ });
54
+
55
+ pool.on('remove', () => {
56
+ console.log('[DB] Client removed. Total:', pool.totalCount, 'Idle:', pool.idleCount);
57
+ });
58
+ }
59
+
60
+ // Graceful shutdown
61
+ async function closePool() {
62
+ if (pool) {
63
+ console.log('[DB] Closing pool...');
64
+ await pool.end();
65
+ console.log('[DB] Pool closed.');
66
+ }
67
+ }
68
+
69
+ // Export the shared pool
70
+ module.exports = {
71
+ pool,
72
+ closePool,
73
+ // Convenience method to get a client for transactions
74
+ getClient: async () => pool ? pool.connect() : null,
75
+ // Convenience method to run a query
76
+ query: (...args) => pool ? pool.query(...args) : Promise.reject(new Error('Database not configured')),
77
+ };
78
+
79
+