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,239 @@
1
+ /**
2
+ * ๐Ÿ’ฐ Promo Treasury Service
3
+ *
4
+ * Manages the treasury wallet for sponsored game joins.
5
+ * This wallet pays buy-ins on behalf of users who have promo codes.
6
+ *
7
+ * Security considerations:
8
+ * - Treasury keypair should be loaded from secure environment variable in production
9
+ * - File-based loading only for local development/testing
10
+ * - Treasury balance should be monitored and alerts set for low balance
11
+ */
12
+
13
+ const { Connection, Keypair, PublicKey, LAMPORTS_PER_SOL } = require('@solana/web3.js');
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+
17
+ // Configuration
18
+ const DEFAULT_KEYPAIR_PATH = path.join(__dirname, '../../dubs/secure_backups/promo_treasury_devnet.json');
19
+ const MIN_BALANCE_WARNING_SOL = 1.0; // Warn when below this balance
20
+
21
+ let treasuryKeypair = null;
22
+ let connection = null;
23
+
24
+ /**
25
+ * Initialize the treasury service
26
+ * @param {Connection} conn - Solana connection
27
+ * @param {Object} options - Initialization options
28
+ */
29
+ function initialize(conn, options = {}) {
30
+ connection = conn;
31
+
32
+ const {
33
+ keypairPath = process.env.PROMO_TREASURY_KEYPAIR_PATH || DEFAULT_KEYPAIR_PATH,
34
+ keypairJson = process.env.PROMO_TREASURY_KEYPAIR // JSON string of secret key array
35
+ } = options;
36
+
37
+ // Try loading from environment variable first (production)
38
+ if (keypairJson) {
39
+ try {
40
+ const secretKey = JSON.parse(keypairJson);
41
+ treasuryKeypair = Keypair.fromSecretKey(Uint8Array.from(secretKey));
42
+ console.log(`[PromoTreasury] โœ… Treasury loaded from environment variable`);
43
+ console.log(`[PromoTreasury] Address: ${treasuryKeypair.publicKey.toBase58()}`);
44
+ return true;
45
+ } catch (error) {
46
+ console.error('[PromoTreasury] โŒ Error loading from env:', error.message);
47
+ }
48
+ }
49
+
50
+ // Fall back to file-based loading (development)
51
+ if (fs.existsSync(keypairPath)) {
52
+ try {
53
+ const secretKey = JSON.parse(fs.readFileSync(keypairPath, 'utf-8'));
54
+ treasuryKeypair = Keypair.fromSecretKey(Uint8Array.from(secretKey));
55
+ console.log(`[PromoTreasury] โœ… Treasury loaded from file: ${keypairPath}`);
56
+ console.log(`[PromoTreasury] Address: ${treasuryKeypair.publicKey.toBase58()}`);
57
+ return true;
58
+ } catch (error) {
59
+ console.error(`[PromoTreasury] โŒ Error loading from file:`, error.message);
60
+ }
61
+ }
62
+
63
+ console.warn(`[PromoTreasury] โš ๏ธ Treasury not initialized - promo codes will not work`);
64
+ console.warn(`[PromoTreasury] Set PROMO_TREASURY_KEYPAIR env var or provide keypair file`);
65
+ return false;
66
+ }
67
+
68
+ /**
69
+ * Check if treasury is initialized and ready
70
+ * @returns {boolean}
71
+ */
72
+ function isReady() {
73
+ return treasuryKeypair !== null && connection !== null;
74
+ }
75
+
76
+ /**
77
+ * Get treasury public key
78
+ * @returns {PublicKey|null}
79
+ */
80
+ function getPublicKey() {
81
+ return treasuryKeypair?.publicKey || null;
82
+ }
83
+
84
+ /**
85
+ * Get treasury keypair for signing transactions
86
+ * WARNING: Handle with care - this returns the full keypair with private key
87
+ * @returns {Keypair|null}
88
+ */
89
+ function getKeypair() {
90
+ return treasuryKeypair;
91
+ }
92
+
93
+ /**
94
+ * Get current treasury balance
95
+ * @returns {Object} - Balance in lamports and SOL
96
+ */
97
+ async function getBalance() {
98
+ if (!isReady()) {
99
+ throw new Error('Treasury not initialized');
100
+ }
101
+
102
+ try {
103
+ const lamports = await connection.getBalance(treasuryKeypair.publicKey);
104
+ const sol = lamports / LAMPORTS_PER_SOL;
105
+
106
+ // Log warning if balance is low
107
+ if (sol < MIN_BALANCE_WARNING_SOL) {
108
+ console.warn(`[PromoTreasury] โš ๏ธ LOW BALANCE: ${sol.toFixed(4)} SOL`);
109
+ }
110
+
111
+ return {
112
+ lamports,
113
+ sol,
114
+ isLow: sol < MIN_BALANCE_WARNING_SOL,
115
+ address: treasuryKeypair.publicKey.toBase58()
116
+ };
117
+
118
+ } catch (error) {
119
+ console.error('[PromoTreasury] Error getting balance:', error.message);
120
+ throw error;
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Check if treasury has enough balance for a sponsored join
126
+ * @param {number} amountLamports - Required amount in lamports
127
+ * @returns {Object} - Check result
128
+ */
129
+ async function hasEnoughBalance(amountLamports) {
130
+ if (!isReady()) {
131
+ return { enough: false, error: 'Treasury not initialized' };
132
+ }
133
+
134
+ try {
135
+ const balance = await getBalance();
136
+
137
+ // Need extra for transaction fees (~5000 lamports per tx + rent if realloc)
138
+ const requiredWithBuffer = amountLamports + 100_000; // 0.0001 SOL buffer for fees
139
+
140
+ if (balance.lamports < requiredWithBuffer) {
141
+ return {
142
+ enough: false,
143
+ error: 'Treasury has insufficient funds',
144
+ currentBalance: balance.lamports,
145
+ required: requiredWithBuffer,
146
+ shortfall: requiredWithBuffer - balance.lamports
147
+ };
148
+ }
149
+
150
+ return {
151
+ enough: true,
152
+ currentBalance: balance.lamports,
153
+ required: requiredWithBuffer,
154
+ remaining: balance.lamports - requiredWithBuffer
155
+ };
156
+
157
+ } catch (error) {
158
+ console.error('[PromoTreasury] Error checking balance:', error.message);
159
+ return { enough: false, error: error.message };
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Get treasury info for admin dashboard
165
+ * @returns {Object} - Treasury wallet info
166
+ */
167
+ async function getTreasuryInfo() {
168
+ if (!isReady()) {
169
+ throw new Error('Treasury not initialized');
170
+ }
171
+
172
+ try {
173
+ const lamports = await connection.getBalance(treasuryKeypair.publicKey);
174
+ const sol = lamports / LAMPORTS_PER_SOL;
175
+
176
+ // Determine network from connection endpoint
177
+ const endpoint = connection.rpcEndpoint || '';
178
+ let network = 'unknown';
179
+ if (endpoint.includes('devnet')) network = 'devnet';
180
+ else if (endpoint.includes('mainnet')) network = 'mainnet-beta';
181
+ else if (endpoint.includes('localhost') || endpoint.includes('127.0.0.1')) network = 'localnet';
182
+
183
+ return {
184
+ address: treasuryKeypair.publicKey.toBase58(),
185
+ balanceLamports: lamports,
186
+ balanceSOL: sol,
187
+ isLow: sol < MIN_BALANCE_WARNING_SOL,
188
+ minBalanceWarning: MIN_BALANCE_WARNING_SOL,
189
+ network,
190
+ };
191
+
192
+ } catch (error) {
193
+ console.error('[PromoTreasury] Error getting treasury info:', error.message);
194
+ throw error;
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Get treasury status/health check
200
+ * @returns {Object} - Status information
201
+ */
202
+ async function getStatus() {
203
+ const status = {
204
+ initialized: isReady(),
205
+ address: getPublicKey()?.toBase58() || null,
206
+ balance: null,
207
+ healthy: false
208
+ };
209
+
210
+ if (status.initialized) {
211
+ try {
212
+ status.balance = await getBalance();
213
+ status.healthy = !status.balance.isLow;
214
+ } catch (error) {
215
+ status.error = error.message;
216
+ }
217
+ }
218
+
219
+ return status;
220
+ }
221
+
222
+ module.exports = {
223
+ // Configuration
224
+ MIN_BALANCE_WARNING_SOL,
225
+
226
+ // Initialization
227
+ initialize,
228
+ isReady,
229
+
230
+ // Wallet access
231
+ getPublicKey,
232
+ getKeypair,
233
+
234
+ // Balance checks
235
+ getBalance,
236
+ hasEnoughBalance,
237
+ getStatus,
238
+ getTreasuryInfo,
239
+ };
@@ -0,0 +1,353 @@
1
+ // ๐Ÿ”” Web Push Notification Service
2
+ // Sends push notifications to PWA/seeker mode users
3
+
4
+ const webpush = require('web-push');
5
+ const urlHelper = require('../utils/urlHelper');
6
+
7
+ // VAPID keys for Web Push authentication
8
+ const VAPID_PUBLIC_KEY = process.env.VAPID_PUBLIC_KEY;
9
+ const VAPID_PRIVATE_KEY = process.env.VAPID_PRIVATE_KEY;
10
+ const VAPID_SUBJECT = process.env.VAPID_SUBJECT || 'mailto:support@dubs.app';
11
+
12
+ let isInitialized = false;
13
+
14
+ /**
15
+ * Initialize web-push with VAPID keys
16
+ * Call this once on server startup
17
+ */
18
+ function initializeWebPush() {
19
+ if (!VAPID_PUBLIC_KEY || !VAPID_PRIVATE_KEY) {
20
+ console.log('[PushNotif] โš ๏ธ VAPID keys not set - push notifications disabled');
21
+ console.log('[PushNotif] โ„น๏ธ Generate keys with: npx web-push generate-vapid-keys');
22
+ return false;
23
+ }
24
+
25
+ try {
26
+ webpush.setVapidDetails(
27
+ VAPID_SUBJECT,
28
+ VAPID_PUBLIC_KEY,
29
+ VAPID_PRIVATE_KEY
30
+ );
31
+ isInitialized = true;
32
+ console.log('[PushNotif] โœ… Web Push initialized with VAPID keys');
33
+ return true;
34
+ } catch (error) {
35
+ console.error('[PushNotif] โŒ Failed to initialize web-push:', error.message);
36
+ return false;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Get the VAPID public key (needed by client to subscribe)
42
+ */
43
+ function getVapidPublicKey() {
44
+ return VAPID_PUBLIC_KEY;
45
+ }
46
+
47
+ /**
48
+ * Send a push notification to a subscription
49
+ * @param {object} subscription - Push subscription object { endpoint, keys: { p256dh, auth } }
50
+ * @param {object} payload - Notification payload { title, body, icon, data }
51
+ * @returns {Promise<boolean>} - true if sent successfully
52
+ */
53
+ async function sendPushNotification(subscription, payload) {
54
+ if (!isInitialized) {
55
+ console.log('[PushNotif] โš ๏ธ Web Push not initialized - skipping notification');
56
+ return false;
57
+ }
58
+
59
+ try {
60
+ const pushPayload = JSON.stringify({
61
+ title: payload.title || 'Dubs',
62
+ body: payload.body || '',
63
+ icon: payload.icon || '/icon-192.png',
64
+ badge: payload.badge || '/badge-72.png',
65
+ data: payload.data || {},
66
+ tag: payload.tag || 'dubs-notification',
67
+ requireInteraction: payload.requireInteraction || false,
68
+ });
69
+
70
+ console.log(`[PushNotif] ๐Ÿš€ Sending push to endpoint: ${subscription.endpoint.substring(0, 50)}...`);
71
+
72
+ await webpush.sendNotification(subscription, pushPayload);
73
+
74
+ console.log('[PushNotif] โœ… Push notification sent successfully');
75
+ return true;
76
+ } catch (error) {
77
+ // Handle specific error codes
78
+ if (error.statusCode === 410 || error.statusCode === 404) {
79
+ // Subscription has expired or is invalid
80
+ console.log('[PushNotif] โš ๏ธ Subscription expired/invalid (will be cleaned up)');
81
+ return { expired: true, endpoint: subscription.endpoint };
82
+ }
83
+
84
+ console.error('[PushNotif] โŒ Failed to send push notification:', error.message);
85
+ return false;
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Forward notification to push if user has subscriptions
91
+ * Mirrors telegramNotifications.forwardChatNotification()
92
+ *
93
+ * @param {object} pool - Database pool
94
+ * @param {number} userId - User ID to send notification to
95
+ * @param {string} notificationType - Type of notification (game_invite, game_joined, etc.)
96
+ * @param {string} senderUsername - Username of the sender
97
+ * @param {string} message - Notification message text
98
+ * @param {object} metadata - Optional metadata (e.g., { gameId: 'sport-xxx' })
99
+ */
100
+ async function forwardPushNotification(pool, userId, notificationType, senderUsername, message = '', metadata = {}) {
101
+ try {
102
+ console.log(`[PushNotif] ๐Ÿ“ค Attempting to forward ${notificationType} notification to user ${userId} from @${senderUsername}`);
103
+
104
+ // Get user's push subscriptions and preferences
105
+ const result = await pool.query(
106
+ `SELECT s.id, s.endpoint, s.p256dh, s.auth, s.device_type, u.username,
107
+ COALESCE(p.notify_reply, true) as notify_reply,
108
+ COALESCE(p.notify_reaction, true) as notify_reaction,
109
+ COALESCE(p.notify_friend_request, true) as notify_friend_request,
110
+ COALESCE(p.notify_friend_request_accepted, true) as notify_friend_request_accepted,
111
+ COALESCE(p.notify_friend_request_declined, true) as notify_friend_request_declined,
112
+ COALESCE(p.notify_referral, true) as notify_referral,
113
+ COALESCE(p.notify_mention, true) as notify_mention,
114
+ COALESCE(p.notify_friend_message, true) as notify_friend_message,
115
+ COALESCE(p.notify_game_joined, true) as notify_game_joined,
116
+ COALESCE(p.notify_game_invite, true) as notify_game_invite
117
+ FROM push_subscriptions s
118
+ JOIN users u ON s.user_id = u.id
119
+ LEFT JOIN push_notification_preferences p ON u.id = p.user_id
120
+ WHERE s.user_id = $1`,
121
+ [userId]
122
+ );
123
+
124
+ if (result.rows.length === 0) {
125
+ console.log(`[PushNotif] โญ๏ธ User ${userId} doesn't have any push subscriptions - skipping`);
126
+ return false;
127
+ }
128
+
129
+ const subscriptions = result.rows;
130
+ const prefs = subscriptions[0]; // All rows have same prefs
131
+ const recipientUsername = prefs.username;
132
+
133
+ console.log(`[PushNotif] ๐Ÿ‘ค Found user @${recipientUsername} with ${subscriptions.length} push subscription(s)`);
134
+
135
+ // Check if user wants this notification type
136
+ const prefMap = {
137
+ 'reply': prefs.notify_reply,
138
+ 'reaction': prefs.notify_reaction,
139
+ 'friend_request': prefs.notify_friend_request,
140
+ 'friend_request_accepted': prefs.notify_friend_request_accepted,
141
+ 'friend_request_declined': prefs.notify_friend_request_declined,
142
+ 'referral': prefs.notify_referral,
143
+ 'mention': prefs.notify_mention,
144
+ 'friend_message': prefs.notify_friend_message,
145
+ 'game_joined': prefs.notify_game_joined,
146
+ 'game_invite': prefs.notify_game_invite,
147
+ 'dm_message': prefs.notify_friend_message, // Use friend_message pref for DMs
148
+ };
149
+
150
+ if (prefMap[notificationType] === false) {
151
+ console.log(`[PushNotif] ๐Ÿ”‡ User @${recipientUsername} has disabled ${notificationType} notifications - skipping`);
152
+ return false;
153
+ }
154
+
155
+ // Format notification payload based on type
156
+ let title = 'Dubs';
157
+ let body = '';
158
+ let icon = '/icon-192.png';
159
+ let tag = 'dubs-notification';
160
+ let url = 'https://dubs.app';
161
+
162
+ switch (notificationType) {
163
+ case 'reply':
164
+ title = `${senderUsername} replied`;
165
+ body = message || 'Replied to your message';
166
+ tag = 'dubs-reply';
167
+ break;
168
+ case 'reaction':
169
+ title = `${senderUsername} reacted ${message}`;
170
+ body = 'To your message';
171
+ tag = 'dubs-reaction';
172
+ break;
173
+ case 'friend_request':
174
+ title = 'New Friend Request';
175
+ body = `${senderUsername} sent you a friend request`;
176
+ tag = 'dubs-friend-request';
177
+ break;
178
+ case 'friend_request_accepted':
179
+ title = 'Friend Request Accepted';
180
+ body = `${senderUsername} accepted your friend request!`;
181
+ tag = 'dubs-friend-accepted';
182
+ break;
183
+ case 'friend_request_declined':
184
+ title = 'Friend Request Declined';
185
+ body = `${senderUsername} declined your friend request`;
186
+ tag = 'dubs-friend-declined';
187
+ break;
188
+ case 'referral':
189
+ title = 'New Referral!';
190
+ body = `${senderUsername} joined using your referral code!`;
191
+ tag = 'dubs-referral';
192
+ break;
193
+ case 'game_joined':
194
+ title = `${senderUsername} joined your bet!`;
195
+ body = message || 'Someone joined your game';
196
+ tag = 'dubs-game-joined';
197
+ if (metadata.gameId) {
198
+ url = urlHelper.getGameShareUrl(metadata.gameId);
199
+ }
200
+ break;
201
+ case 'game_invite':
202
+ title = `${senderUsername} invited you!`;
203
+ body = message || 'You\'ve been invited to join a bet';
204
+ tag = 'dubs-game-invite';
205
+ if (metadata.gameId) {
206
+ url = urlHelper.getGameShareUrl(metadata.gameId);
207
+ }
208
+ break;
209
+ case 'mention':
210
+ title = `${senderUsername} mentioned you`;
211
+ body = message || 'You were mentioned in a message';
212
+ tag = 'dubs-mention';
213
+ break;
214
+ case 'friend_message':
215
+ title = `Message from ${senderUsername}`;
216
+ body = message || 'New message';
217
+ tag = 'dubs-message';
218
+ break;
219
+ case 'dm_message':
220
+ title = `DM from ${senderUsername}`;
221
+ body = message || 'New direct message';
222
+ tag = 'dubs-dm';
223
+ break;
224
+ case 'game_won':
225
+ title = 'You Won!';
226
+ body = message || 'Congratulations on your win!';
227
+ tag = 'dubs-game-won';
228
+ if (metadata.gameId) {
229
+ url = urlHelper.getGameShareUrl(metadata.gameId);
230
+ }
231
+ break;
232
+ case 'game_lost':
233
+ title = 'Game Finished';
234
+ body = message || 'Better luck next time!';
235
+ tag = 'dubs-game-lost';
236
+ if (metadata.gameId) {
237
+ url = urlHelper.getGameShareUrl(metadata.gameId);
238
+ }
239
+ break;
240
+ case 'game_starting_soon':
241
+ title = 'Game Starting Soon!';
242
+ body = message || 'Your game is about to start';
243
+ tag = 'dubs-game-starting';
244
+ if (metadata.gameId) {
245
+ url = urlHelper.getGameShareUrl(metadata.gameId);
246
+ }
247
+ break;
248
+ case 'game_starting_now':
249
+ title = 'Game Starting NOW!';
250
+ body = message || 'Betting is now closed';
251
+ tag = 'dubs-game-live';
252
+ if (metadata.gameId) {
253
+ url = urlHelper.getGameShareUrl(metadata.gameId);
254
+ }
255
+ break;
256
+ case 'whats_new':
257
+ title = "What's New on Dubs!";
258
+ body = message || 'Check out the latest updates';
259
+ tag = 'dubs-whats-new';
260
+ if (metadata.postId) {
261
+ url = urlHelper.getWhatsNewUrl(metadata.postId);
262
+ }
263
+ break;
264
+ default:
265
+ title = 'Dubs Notification';
266
+ body = `${senderUsername}: ${notificationType}`;
267
+ }
268
+
269
+ const payload = {
270
+ title,
271
+ body,
272
+ icon,
273
+ tag,
274
+ data: {
275
+ url,
276
+ notificationType,
277
+ ...metadata
278
+ },
279
+ requireInteraction: ['game_invite', 'friend_request', 'game_won'].includes(notificationType),
280
+ };
281
+
282
+ console.log(`[PushNotif] ๐Ÿ“จ Sending ${notificationType} to ${subscriptions.length} device(s)...`);
283
+
284
+ // Send to all user's subscriptions
285
+ const expiredEndpoints = [];
286
+ let successCount = 0;
287
+
288
+ for (const sub of subscriptions) {
289
+ const subscription = {
290
+ endpoint: sub.endpoint,
291
+ keys: {
292
+ p256dh: sub.p256dh,
293
+ auth: sub.auth
294
+ }
295
+ };
296
+
297
+ const result = await sendPushNotification(subscription, payload);
298
+
299
+ if (result === true) {
300
+ successCount++;
301
+ } else if (result && result.expired) {
302
+ expiredEndpoints.push(sub.id);
303
+ }
304
+ }
305
+
306
+ // Clean up expired subscriptions
307
+ if (expiredEndpoints.length > 0) {
308
+ console.log(`[PushNotif] ๐Ÿงน Cleaning up ${expiredEndpoints.length} expired subscription(s)`);
309
+ await pool.query(
310
+ 'DELETE FROM push_subscriptions WHERE id = ANY($1)',
311
+ [expiredEndpoints]
312
+ );
313
+ }
314
+
315
+ if (successCount > 0) {
316
+ console.log(`[PushNotif] โœ… Successfully sent ${notificationType} to ${successCount}/${subscriptions.length} device(s) for @${recipientUsername}`);
317
+ return true;
318
+ } else {
319
+ console.log(`[PushNotif] โŒ Failed to send ${notificationType} to any device for @${recipientUsername}`);
320
+ return false;
321
+ }
322
+ } catch (error) {
323
+ console.error(`[PushNotif] โŒ Error forwarding ${notificationType} notification:`, error.message);
324
+ return false;
325
+ }
326
+ }
327
+
328
+ /**
329
+ * Check if a user has push notifications enabled
330
+ * @param {object} pool - Database pool
331
+ * @param {number} userId - User ID to check
332
+ * @returns {Promise<boolean>}
333
+ */
334
+ async function userHasPushSubscription(pool, userId) {
335
+ try {
336
+ const result = await pool.query(
337
+ 'SELECT COUNT(*) as count FROM push_subscriptions WHERE user_id = $1',
338
+ [userId]
339
+ );
340
+ return parseInt(result.rows[0].count) > 0;
341
+ } catch (error) {
342
+ console.error('[PushNotif] Error checking push subscription:', error.message);
343
+ return false;
344
+ }
345
+ }
346
+
347
+ module.exports = {
348
+ initializeWebPush,
349
+ getVapidPublicKey,
350
+ sendPushNotification,
351
+ forwardPushNotification,
352
+ userHasPushSubscription
353
+ };