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,374 @@
1
+ /**
2
+ * Connect4 Notification Service
3
+ * Handles in-database notifications and Telegram forwarding for Connect4 games
4
+ */
5
+
6
+ const { forwardChatNotification } = require('./telegramNotifications');
7
+
8
+ /**
9
+ * Send "Your Turn" notification to the opponent after a move
10
+ * @param {object} pool - Database pool
11
+ * @param {object} chatNamespace - Socket.io chat namespace
12
+ * @param {object} params - Notification parameters
13
+ * @param {string} params.gameId - Connect4 game ID
14
+ * @param {string} params.opponentWallet - Wallet address of opponent (who needs to move)
15
+ * @param {string} params.moverWallet - Wallet address of player who just moved
16
+ * @param {string} params.moverUsername - Username of player who just moved
17
+ * @param {number} params.buyIn - Game buy-in amount
18
+ */
19
+ async function notifyYourTurn(pool, chatNamespace, params) {
20
+ const { gameId, opponentWallet, moverWallet, moverUsername, buyIn } = params;
21
+
22
+ try {
23
+ // Look up opponent's user ID
24
+ const userResult = await pool.query(
25
+ 'SELECT id, username FROM users WHERE wallet_address = $1',
26
+ [opponentWallet]
27
+ );
28
+
29
+ if (userResult.rows.length === 0) {
30
+ console.log(`[Connect4Notif] Opponent ${opponentWallet.slice(0, 8)} not found in users table - skipping notification`);
31
+ return;
32
+ }
33
+
34
+ const opponentUserId = userResult.rows[0].id;
35
+ const opponentUsername = userResult.rows[0].username;
36
+
37
+ // Look up mover's user ID
38
+ const moverResult = await pool.query(
39
+ 'SELECT id FROM users WHERE wallet_address = $1',
40
+ [moverWallet]
41
+ );
42
+ const moverUserId = moverResult.rows.length > 0 ? moverResult.rows[0].id : null;
43
+
44
+ // Build notification data with gameInvite structure (frontend expects this)
45
+ const notificationData = {
46
+ gameInvite: {
47
+ gameId,
48
+ gameAddress: '', // Not needed for notification display
49
+ title: '4 the Pot',
50
+ gameType: 'connect4',
51
+ buyIn,
52
+ creatorUsername: moverUsername,
53
+ creatorWallet: moverWallet,
54
+ status: 'playing',
55
+ },
56
+ };
57
+
58
+ // Insert notification into database
59
+ const insertResult = await pool.query(
60
+ `INSERT INTO chat_notifications (
61
+ user_id, sender_user_id, notification_type, notification_data, read, created_at
62
+ ) VALUES ($1, $2, 'connect4_your_turn', $3, false, NOW())
63
+ RETURNING id, created_at`,
64
+ [opponentUserId, moverUserId, JSON.stringify(notificationData)]
65
+ );
66
+
67
+ const notificationId = insertResult.rows[0].id;
68
+ const notificationCreatedAt = insertResult.rows[0].created_at;
69
+
70
+ console.log(`[Connect4Notif] Sent connect4_your_turn notification to ${opponentUsername} (ID: ${notificationId})`);
71
+
72
+ // Send real-time notification via WebSocket
73
+ if (chatNamespace) {
74
+ const notification = {
75
+ id: notificationId,
76
+ type: 'connect4_your_turn',
77
+ senderUsername: moverUsername,
78
+ senderWallet: moverWallet,
79
+ message: '',
80
+ gameInvite: notificationData.gameInvite,
81
+ createdAt: notificationCreatedAt.toISOString(),
82
+ read: false,
83
+ };
84
+
85
+ chatNamespace.to(`user-${opponentUserId}`).emit('notification', notification);
86
+ console.log(`[Connect4Notif] Real-time notification sent to user-${opponentUserId}`);
87
+ }
88
+
89
+ // Forward to Telegram
90
+ try {
91
+ const message = `${moverUsername} made a move - it's your turn!`;
92
+ await forwardChatNotification(pool, opponentUserId, 'connect4_your_turn', moverUsername, message, { gameId });
93
+ } catch (telegramError) {
94
+ console.log(`[Connect4Notif] Telegram forward failed:`, telegramError.message);
95
+ }
96
+
97
+ } catch (error) {
98
+ console.error('[Connect4Notif] Error sending your turn notification:', error.message);
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Send game result notifications (win/lose) to both players
104
+ * @param {object} pool - Database pool
105
+ * @param {object} chatNamespace - Socket.io chat namespace
106
+ * @param {object} params - Notification parameters
107
+ * @param {string} params.gameId - Connect4 game ID
108
+ * @param {string} params.winnerWallet - Winner's wallet address (null for draw)
109
+ * @param {string} params.winnerUsername - Winner's username
110
+ * @param {string} params.loserWallet - Loser's wallet address (null for draw)
111
+ * @param {string} params.loserUsername - Loser's username
112
+ * @param {number} params.buyIn - Game buy-in amount
113
+ * @param {number} params.winnerPrize - Prize amount for winner
114
+ * @param {boolean} params.isDraw - Whether the game was a draw
115
+ */
116
+ async function notifyGameResult(pool, chatNamespace, params) {
117
+ const { gameId, winnerWallet, winnerUsername, loserWallet, loserUsername, buyIn, winnerPrize, isDraw } = params;
118
+
119
+ try {
120
+ // Build gameInvite structure for notification display
121
+ const gameInvite = {
122
+ gameId,
123
+ gameAddress: '',
124
+ title: '4 the Pot',
125
+ gameType: 'connect4',
126
+ buyIn,
127
+ status: 'completed',
128
+ };
129
+
130
+ // For draws, notify both players with game_lost (they can claim refund)
131
+ if (isDraw) {
132
+ await notifyPlayer(pool, chatNamespace, {
133
+ walletAddress: winnerWallet, // In draw context, this is player1
134
+ notificationType: 'game_lost',
135
+ gameInvite,
136
+ finalScore: { winner: null, homeScore: 0, awayScore: 0 },
137
+ message: 'Game ended in a draw - claim your refund!',
138
+ gameId,
139
+ });
140
+
141
+ await notifyPlayer(pool, chatNamespace, {
142
+ walletAddress: loserWallet, // In draw context, this is player2
143
+ notificationType: 'game_lost',
144
+ gameInvite,
145
+ finalScore: { winner: null, homeScore: 0, awayScore: 0 },
146
+ message: 'Game ended in a draw - claim your refund!',
147
+ gameId,
148
+ });
149
+
150
+ return;
151
+ }
152
+
153
+ // Notify winner
154
+ if (winnerWallet) {
155
+ await notifyPlayer(pool, chatNamespace, {
156
+ walletAddress: winnerWallet,
157
+ notificationType: 'game_won',
158
+ gameInvite,
159
+ finalScore: { winner: 'home', homeScore: 1, awayScore: 0 }, // Simplified for Connect4
160
+ message: `You won ${winnerPrize?.toFixed(2) || buyIn * 2 * 0.95} SOL!`,
161
+ gameId,
162
+ senderUsername: loserUsername,
163
+ senderWallet: loserWallet,
164
+ });
165
+ }
166
+
167
+ // Notify loser
168
+ if (loserWallet) {
169
+ await notifyPlayer(pool, chatNamespace, {
170
+ walletAddress: loserWallet,
171
+ notificationType: 'game_lost',
172
+ gameInvite,
173
+ finalScore: { winner: 'away', homeScore: 0, awayScore: 1 },
174
+ message: 'Better luck next time!',
175
+ gameId,
176
+ senderUsername: winnerUsername,
177
+ senderWallet: winnerWallet,
178
+ });
179
+ }
180
+
181
+ } catch (error) {
182
+ console.error('[Connect4Notif] Error sending game result notifications:', error.message);
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Helper to notify a single player
188
+ */
189
+ async function notifyPlayer(pool, chatNamespace, params) {
190
+ const { walletAddress, notificationType, gameInvite, finalScore, message, gameId, senderUsername, senderWallet } = params;
191
+
192
+ try {
193
+ // Look up user
194
+ const userResult = await pool.query(
195
+ 'SELECT id, username FROM users WHERE wallet_address = $1',
196
+ [walletAddress]
197
+ );
198
+
199
+ if (userResult.rows.length === 0) {
200
+ console.log(`[Connect4Notif] User ${walletAddress.slice(0, 8)} not found - skipping`);
201
+ return;
202
+ }
203
+
204
+ const userId = userResult.rows[0].id;
205
+ const username = userResult.rows[0].username;
206
+
207
+ // Look up sender's user ID if provided
208
+ let senderUserId = null;
209
+ if (senderWallet) {
210
+ const senderResult = await pool.query(
211
+ 'SELECT id FROM users WHERE wallet_address = $1',
212
+ [senderWallet]
213
+ );
214
+ senderUserId = senderResult.rows.length > 0 ? senderResult.rows[0].id : null;
215
+ }
216
+
217
+ // Build notification data
218
+ const notificationData = {
219
+ gameInvite,
220
+ finalScore,
221
+ message,
222
+ };
223
+
224
+ // Insert notification
225
+ const insertResult = await pool.query(
226
+ `INSERT INTO chat_notifications (
227
+ user_id, sender_user_id, notification_type, notification_data, read, created_at
228
+ ) VALUES ($1, $2, $3, $4, false, NOW())
229
+ RETURNING id, created_at`,
230
+ [userId, senderUserId, notificationType, JSON.stringify(notificationData)]
231
+ );
232
+
233
+ const notificationId = insertResult.rows[0].id;
234
+ const notificationCreatedAt = insertResult.rows[0].created_at;
235
+
236
+ console.log(`[Connect4Notif] Sent ${notificationType} notification to ${username} (ID: ${notificationId})`);
237
+
238
+ // Send real-time notification via WebSocket
239
+ if (chatNamespace) {
240
+ const notification = {
241
+ id: notificationId,
242
+ type: notificationType,
243
+ senderUsername: senderUsername || '4 the Pot',
244
+ senderWallet: senderWallet || '',
245
+ message: message || '',
246
+ gameInvite,
247
+ finalScore,
248
+ createdAt: notificationCreatedAt.toISOString(),
249
+ read: false,
250
+ };
251
+
252
+ chatNamespace.to(`user-${userId}`).emit('notification', notification);
253
+ console.log(`[Connect4Notif] Real-time ${notificationType} sent to user-${userId}`);
254
+ }
255
+
256
+ // Forward to Telegram
257
+ try {
258
+ await forwardChatNotification(pool, userId, notificationType, senderUsername || '4 the Pot', message, { gameId });
259
+ } catch (telegramError) {
260
+ console.log(`[Connect4Notif] Telegram forward failed for ${username}:`, telegramError.message);
261
+ }
262
+
263
+ } catch (error) {
264
+ console.error(`[Connect4Notif] Error notifying player ${walletAddress.slice(0, 8)}:`, error.message);
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Send cancellation notification to the invited player when creator cancels
270
+ * @param {object} pool - Database pool
271
+ * @param {object} chatNamespace - Socket.io chat namespace
272
+ * @param {object} params - Notification parameters
273
+ * @param {string} params.gameId - Connect4 game ID
274
+ * @param {string} params.invitedPlayerWallet - Wallet address of invited player
275
+ * @param {string} params.creatorWallet - Creator's wallet address
276
+ * @param {string} params.creatorUsername - Creator's username
277
+ * @param {number} params.buyIn - Game buy-in amount
278
+ */
279
+ async function notifyGameCancelled(pool, chatNamespace, params) {
280
+ const { gameId, invitedPlayerWallet, creatorWallet, creatorUsername, buyIn } = params;
281
+
282
+ if (!invitedPlayerWallet) {
283
+ console.log(`[Connect4Notif] No invited player for game ${gameId} - skipping cancel notification`);
284
+ return;
285
+ }
286
+
287
+ try {
288
+ // Look up invited player's user ID
289
+ const userResult = await pool.query(
290
+ 'SELECT id, username FROM users WHERE wallet_address = $1',
291
+ [invitedPlayerWallet]
292
+ );
293
+
294
+ if (userResult.rows.length === 0) {
295
+ console.log(`[Connect4Notif] Invited player ${invitedPlayerWallet.slice(0, 8)} not found - skipping`);
296
+ return;
297
+ }
298
+
299
+ const invitedUserId = userResult.rows[0].id;
300
+ const invitedUsername = userResult.rows[0].username;
301
+
302
+ // Look up creator's user ID and avatar
303
+ const creatorResult = await pool.query(
304
+ 'SELECT id, avatar FROM users WHERE wallet_address = $1',
305
+ [creatorWallet]
306
+ );
307
+ const creatorUserId = creatorResult.rows.length > 0 ? creatorResult.rows[0].id : null;
308
+ const creatorAvatar = creatorResult.rows.length > 0 ? creatorResult.rows[0].avatar : null;
309
+
310
+ // Build notification data with gameInvite structure
311
+ const notificationData = {
312
+ gameInvite: {
313
+ gameId,
314
+ gameAddress: '',
315
+ title: '4 the Pot',
316
+ gameType: 'connect4',
317
+ buyIn,
318
+ creatorUsername,
319
+ creatorWallet,
320
+ status: 'cancelled',
321
+ },
322
+ message: `${creatorUsername} cancelled the game`,
323
+ };
324
+
325
+ // Insert notification into database
326
+ const insertResult = await pool.query(
327
+ `INSERT INTO chat_notifications (
328
+ user_id, sender_user_id, notification_type, notification_data, read, created_at
329
+ ) VALUES ($1, $2, 'game_cancelled', $3, false, NOW())
330
+ RETURNING id, created_at`,
331
+ [invitedUserId, creatorUserId, JSON.stringify(notificationData)]
332
+ );
333
+
334
+ const notificationId = insertResult.rows[0].id;
335
+ const notificationCreatedAt = insertResult.rows[0].created_at;
336
+
337
+ console.log(`[Connect4Notif] Sent game_cancelled notification to ${invitedUsername} (ID: ${notificationId})`);
338
+
339
+ // Send real-time notification via WebSocket
340
+ if (chatNamespace) {
341
+ const notification = {
342
+ id: notificationId,
343
+ type: 'game_cancelled',
344
+ senderUsername: creatorUsername,
345
+ senderWallet: creatorWallet,
346
+ senderAvatar: creatorAvatar,
347
+ message: `${creatorUsername} cancelled the game`,
348
+ gameInvite: notificationData.gameInvite,
349
+ createdAt: notificationCreatedAt.toISOString(),
350
+ read: false,
351
+ };
352
+
353
+ chatNamespace.to(`user-${invitedUserId}`).emit('notification', notification);
354
+ console.log(`[Connect4Notif] Real-time cancel notification sent to user-${invitedUserId}`);
355
+ }
356
+
357
+ // Forward to Telegram
358
+ try {
359
+ const message = `${creatorUsername} cancelled the Connect 4 game invite`;
360
+ await forwardChatNotification(pool, invitedUserId, 'game_lost', creatorUsername, message, { gameId });
361
+ } catch (telegramError) {
362
+ console.log(`[Connect4Notif] Telegram forward failed:`, telegramError.message);
363
+ }
364
+
365
+ } catch (error) {
366
+ console.error('[Connect4Notif] Error sending cancel notification:', error.message);
367
+ }
368
+ }
369
+
370
+ module.exports = {
371
+ notifyYourTurn,
372
+ notifyGameResult,
373
+ notifyGameCancelled,
374
+ };
@@ -0,0 +1,223 @@
1
+ const axios = require('axios');
2
+ const { pool } = require('./db'); // Shared database pool
3
+
4
+ class CryptoPriceService {
5
+ constructor() {
6
+ this.coingeckoApiUrl = 'https://api.coingecko.com/api/v3';
7
+ this.cacheTTL = parseInt(process.env.CRYPTO_PRICE_CACHE_TTL) || 300; // 5 minutes in seconds
8
+ this.supportedCryptos = ['solana']; // CoinGecko IDs
9
+
10
+ // Initialize cache table on startup
11
+ this.initializeCacheTable();
12
+ }
13
+
14
+ /**
15
+ * Initialize the crypto_prices_cache table if it doesn't exist
16
+ */
17
+ async initializeCacheTable() {
18
+ try {
19
+ await pool.query(`
20
+ CREATE TABLE IF NOT EXISTS crypto_prices_cache (
21
+ id SERIAL PRIMARY KEY,
22
+ crypto_id VARCHAR(50) NOT NULL UNIQUE,
23
+ prices JSONB NOT NULL,
24
+ last_updated TIMESTAMP NOT NULL DEFAULT NOW(),
25
+ expires_at TIMESTAMP NOT NULL,
26
+ created_at TIMESTAMP DEFAULT NOW()
27
+ );
28
+
29
+ CREATE INDEX IF NOT EXISTS idx_crypto_prices_id ON crypto_prices_cache(crypto_id);
30
+ CREATE INDEX IF NOT EXISTS idx_crypto_prices_expires ON crypto_prices_cache(expires_at);
31
+ `);
32
+ console.log('[Crypto Prices] Cache table initialized');
33
+ } catch (error) {
34
+ console.error('[Crypto Prices] Error initializing cache table:', error);
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Get crypto price in multiple fiat currencies
40
+ * @param {string} cryptoId - CoinGecko crypto ID (e.g., 'solana')
41
+ * @param {string[]} vsCurrencies - Array of currency codes to get prices in
42
+ * @returns {Object} Prices object
43
+ */
44
+ async getCryptoPrice(cryptoId = 'solana', vsCurrencies = ['usd', 'eur', 'gbp', 'cad', 'jpy', 'aud']) {
45
+ try {
46
+ // Try to get from cache first
47
+ const cachedPrice = await this.getCachedPrice(cryptoId);
48
+ if (cachedPrice) {
49
+ console.log(`[Crypto Prices] Retrieved ${cryptoId} from cache`);
50
+ return {
51
+ ...cachedPrice,
52
+ source: 'cache'
53
+ };
54
+ }
55
+
56
+ // If not in cache, fetch from CoinGecko
57
+ console.log(`[Crypto Prices] Fetching fresh price for ${cryptoId} from CoinGecko`);
58
+ const freshPrice = await this.fetchFromCoinGecko(cryptoId, vsCurrencies);
59
+
60
+ // Cache the fresh price
61
+ await this.cachePrice(cryptoId, freshPrice);
62
+
63
+ return {
64
+ ...freshPrice,
65
+ source: 'api'
66
+ };
67
+ } catch (error) {
68
+ console.error('[Crypto Prices] Error getting crypto price:', error);
69
+ throw new Error('Failed to retrieve crypto price');
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Get SOL price in USD (convenience method)
75
+ * @returns {number} SOL price in USD
76
+ */
77
+ async getSOLPriceUSD() {
78
+ const priceData = await this.getCryptoPrice('solana', ['usd']);
79
+ return priceData.prices.usd;
80
+ }
81
+
82
+ /**
83
+ * Get cached crypto price from PostgreSQL
84
+ * @param {string} cryptoId - Crypto ID
85
+ * @returns {Object|null} Cached price or null
86
+ */
87
+ async getCachedPrice(cryptoId) {
88
+ try {
89
+ const result = await pool.query(
90
+ 'SELECT prices, last_updated FROM crypto_prices_cache WHERE crypto_id = $1 AND expires_at > NOW()',
91
+ [cryptoId]
92
+ );
93
+
94
+ if (result.rows.length > 0) {
95
+ const row = result.rows[0];
96
+ return {
97
+ cryptoId,
98
+ prices: row.prices,
99
+ lastUpdated: row.last_updated,
100
+ timestamp: new Date(row.last_updated).getTime()
101
+ };
102
+ }
103
+
104
+ return null;
105
+ } catch (error) {
106
+ console.error('[Crypto Prices] Error getting cached price:', error);
107
+ return null;
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Cache crypto price in PostgreSQL
113
+ * @param {string} cryptoId - Crypto ID
114
+ * @param {Object} priceData - Price data
115
+ */
116
+ async cachePrice(cryptoId, priceData) {
117
+ try {
118
+ const expiresAt = new Date(Date.now() + this.cacheTTL * 1000);
119
+
120
+ await pool.query(`
121
+ INSERT INTO crypto_prices_cache (crypto_id, prices, last_updated, expires_at)
122
+ VALUES ($1, $2, NOW(), $3)
123
+ ON CONFLICT (crypto_id)
124
+ DO UPDATE SET
125
+ prices = $2,
126
+ last_updated = NOW(),
127
+ expires_at = $3
128
+ `, [cryptoId, JSON.stringify(priceData.prices), expiresAt]);
129
+
130
+ console.log(`[Crypto Prices] Cached ${cryptoId} prices with TTL: ${this.cacheTTL}s`);
131
+ } catch (error) {
132
+ console.error('[Crypto Prices] Error caching price:', error);
133
+ // Don't throw error, caching failure shouldn't break the service
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Fetch crypto prices from CoinGecko API
139
+ * @param {string} cryptoId - CoinGecko crypto ID
140
+ * @param {string[]} vsCurrencies - Array of currency codes
141
+ * @returns {Object} Fresh crypto prices
142
+ */
143
+ async fetchFromCoinGecko(cryptoId, vsCurrencies) {
144
+ try {
145
+ const currenciesParam = vsCurrencies.join(',');
146
+ const url = `${this.coingeckoApiUrl}/simple/price?ids=${cryptoId}&vs_currencies=${currenciesParam}`;
147
+
148
+ const response = await axios.get(url, {
149
+ timeout: 10000, // 10 second timeout
150
+ headers: {
151
+ 'User-Agent': 'Dubs-Crypto-Price-API/1.0',
152
+ 'Accept': 'application/json'
153
+ }
154
+ });
155
+
156
+ if (!response.data || !response.data[cryptoId]) {
157
+ throw new Error('Invalid response from CoinGecko API');
158
+ }
159
+
160
+ const prices = response.data[cryptoId];
161
+
162
+ return {
163
+ cryptoId,
164
+ prices,
165
+ timestamp: Date.now(),
166
+ lastUpdated: new Date().toISOString()
167
+ };
168
+ } catch (error) {
169
+ if (error.response) {
170
+ console.error('[Crypto Prices] CoinGecko API Error:', error.response.status, error.response.data);
171
+
172
+ // Handle specific API errors
173
+ if (error.response.status === 429) {
174
+ throw new Error('CoinGecko API: Rate limit exceeded');
175
+ }
176
+
177
+ throw new Error(`CoinGecko API error: ${error.response.status}`);
178
+ } else if (error.request) {
179
+ console.error('[Crypto Prices] Network Error:', error.message);
180
+ throw new Error('Network error connecting to CoinGecko API');
181
+ } else {
182
+ console.error('[Crypto Prices] Error:', error.message);
183
+ throw new Error('Failed to fetch crypto prices');
184
+ }
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Clear cache for a specific crypto
190
+ * @param {string} cryptoId - Crypto ID
191
+ */
192
+ async clearCache(cryptoId = 'solana') {
193
+ try {
194
+ await pool.query(
195
+ 'DELETE FROM crypto_prices_cache WHERE crypto_id = $1',
196
+ [cryptoId]
197
+ );
198
+ console.log(`[Crypto Prices] Cache cleared for ${cryptoId}`);
199
+ } catch (error) {
200
+ console.error('[Crypto Prices] Error clearing cache:', error);
201
+ throw new Error('Failed to clear cache');
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Clean up expired cache entries
207
+ */
208
+ async cleanupExpiredCache() {
209
+ try {
210
+ const result = await pool.query(
211
+ 'DELETE FROM crypto_prices_cache WHERE expires_at < NOW()'
212
+ );
213
+ if (result.rowCount > 0) {
214
+ console.log(`[Crypto Prices] Cleaned up ${result.rowCount} expired cache entries`);
215
+ }
216
+ } catch (error) {
217
+ console.error('[Crypto Prices] Error cleaning up cache:', error);
218
+ }
219
+ }
220
+ }
221
+
222
+ module.exports = new CryptoPriceService();
223
+