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,245 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Backfill Matchup Images Script
4
+ *
5
+ * Generates matchup images for existing sports games that don't have one.
6
+ * Uploads to S3 and updates the database.
7
+ *
8
+ * Usage:
9
+ * node scripts/backfill-matchup-images.js # Process all games without matchup images
10
+ * node scripts/backfill-matchup-images.js --limit 10 # Process only 10 games
11
+ * node scripts/backfill-matchup-images.js --game-id sport-xxx # Process specific game
12
+ */
13
+
14
+ require('dotenv').config();
15
+ const { Pool } = require('pg');
16
+
17
+ // Lazy load services (will fail if canvas not installed)
18
+ let matchupImageService = null;
19
+ let s3Service = null;
20
+
21
+ function getMatchupImageService() {
22
+ if (!matchupImageService) {
23
+ try {
24
+ matchupImageService = require('../services/matchupImageService');
25
+ console.log('āœ… Matchup image service loaded');
26
+ } catch (err) {
27
+ console.error('āŒ Failed to load matchup image service:', err.message);
28
+ console.error(' Install canvas: npm install canvas');
29
+ process.exit(1);
30
+ }
31
+ }
32
+ return matchupImageService;
33
+ }
34
+
35
+ function getS3Service() {
36
+ if (!s3Service) {
37
+ try {
38
+ const S3Service = require('../services/s3Service');
39
+ s3Service = new S3Service();
40
+ if (!s3Service.isConfigured()) {
41
+ console.error('āŒ S3 credentials not configured');
42
+ console.error(' Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in .env');
43
+ process.exit(1);
44
+ }
45
+ console.log('āœ… S3 service loaded');
46
+ } catch (err) {
47
+ console.error('āŒ Failed to load S3 service:', err.message);
48
+ process.exit(1);
49
+ }
50
+ }
51
+ return s3Service;
52
+ }
53
+
54
+ // Database connection
55
+ const pool = new Pool({
56
+ connectionString: process.env.DATABASE_URL,
57
+ ssl: process.env.DATABASE_URL?.includes('amazonaws') || process.env.DATABASE_URL?.includes('heroku')
58
+ ? { rejectUnauthorized: false }
59
+ : false
60
+ });
61
+
62
+ // League name normalization
63
+ const leagueMap = {
64
+ 'NATIONAL HOCKEY LEAGUE': 'NHL',
65
+ 'NATIONAL BASKETBALL ASSOCIATION': 'NBA',
66
+ 'NATIONAL FOOTBALL LEAGUE': 'NFL',
67
+ 'MAJOR LEAGUE BASEBALL': 'MLB',
68
+ 'ENGLISH PREMIER LEAGUE': 'EPL',
69
+ 'PREMIER LEAGUE': 'EPL',
70
+ 'NHL': 'NHL',
71
+ 'NBA': 'NBA',
72
+ 'NFL': 'NFL',
73
+ 'MLB': 'MLB',
74
+ 'EPL': 'EPL'
75
+ };
76
+
77
+ function normalizeLeague(league) {
78
+ return leagueMap[league?.toUpperCase()] || 'NHL';
79
+ }
80
+
81
+ async function processGame(game) {
82
+ const service = getMatchupImageService();
83
+ const s3 = getS3Service();
84
+
85
+ const sportsEvent = game.sports_event;
86
+ const homeTeam = sportsEvent?.strHomeTeam;
87
+ const awayTeam = sportsEvent?.strAwayTeam;
88
+ const league = normalizeLeague(sportsEvent?.strLeague);
89
+
90
+ if (!homeTeam || !awayTeam) {
91
+ console.log(` āš ļø Skipping ${game.game_id} - missing team names`);
92
+ return { skipped: true };
93
+ }
94
+
95
+ console.log(` šŸ“ Processing: ${awayTeam} @ ${homeTeam} (${league})`);
96
+
97
+ try {
98
+ // Check if image already exists (team-based key allows reuse)
99
+ const matchupKey = s3.getMatchupImageKey(awayTeam, homeTeam, league);
100
+ const exists = await s3.matchupImageExists(matchupKey);
101
+
102
+ let publicUrl;
103
+ if (exists) {
104
+ // Image already exists, reuse it
105
+ publicUrl = `https://${s3.bucketName}.s3.${s3.region}.amazonaws.com/${matchupKey}`;
106
+ console.log(` ā™»ļø Reusing existing image for ${awayTeam} @ ${homeTeam}`);
107
+ } else {
108
+ // Generate image at full quality (600x315), will be resized to 300x158 on upload
109
+ const result = await service.generateMatchupImage({
110
+ homeTeam,
111
+ awayTeam,
112
+ league,
113
+ width: 600,
114
+ height: 315
115
+ });
116
+
117
+ // Upload to S3 (will check again and upload if needed)
118
+ const uploadResult = await s3.uploadMatchupImage(awayTeam, homeTeam, league, result.buffer);
119
+ publicUrl = uploadResult.publicUrl;
120
+
121
+ if (uploadResult.wasReused) {
122
+ console.log(` ā™»ļø Image was created by another process, reusing it`);
123
+ }
124
+ }
125
+
126
+ // Update database
127
+ await pool.query(`
128
+ UPDATE games
129
+ SET matchup_image_url = $1, updated_at = NOW()
130
+ WHERE game_id = $2
131
+ `, [publicUrl, game.game_id]);
132
+
133
+ console.log(` āœ… Saved: ${publicUrl}`);
134
+ return { success: true, url: publicUrl };
135
+
136
+ } catch (err) {
137
+ console.error(` āŒ Failed: ${err.message}`);
138
+ return { error: err.message };
139
+ }
140
+ }
141
+
142
+ async function main() {
143
+ console.log('');
144
+ console.log('šŸŽØ Matchup Image Backfill Script');
145
+ console.log('================================');
146
+ console.log('');
147
+
148
+ // Parse arguments
149
+ const args = process.argv.slice(2);
150
+ let limit = null;
151
+ let gameId = null;
152
+
153
+ for (let i = 0; i < args.length; i++) {
154
+ if (args[i] === '--limit' && args[i + 1]) {
155
+ limit = parseInt(args[i + 1], 10);
156
+ }
157
+ if (args[i] === '--game-id' && args[i + 1]) {
158
+ gameId = args[i + 1];
159
+ }
160
+ }
161
+
162
+ // Initialize services
163
+ getMatchupImageService();
164
+ getS3Service();
165
+
166
+ try {
167
+ // Query for games needing matchup images
168
+ let query;
169
+ let params;
170
+
171
+ if (gameId) {
172
+ query = `
173
+ SELECT game_id, sports_event
174
+ FROM games
175
+ WHERE game_id = $1 AND game_mode = 4
176
+ `;
177
+ params = [gameId];
178
+ } else {
179
+ query = `
180
+ SELECT game_id, sports_event
181
+ FROM games
182
+ WHERE game_mode = 4
183
+ AND matchup_image_url IS NULL
184
+ AND sports_event IS NOT NULL
185
+ ORDER BY created_at DESC
186
+ ${limit ? `LIMIT ${limit}` : ''}
187
+ `;
188
+ params = [];
189
+ }
190
+
191
+ const result = await pool.query(query, params);
192
+ const games = result.rows;
193
+
194
+ console.log(`šŸ“Š Found ${games.length} games to process`);
195
+ console.log('');
196
+
197
+ if (games.length === 0) {
198
+ console.log('āœ… No games need processing!');
199
+ process.exit(0);
200
+ }
201
+
202
+ // Process games
203
+ let processed = 0;
204
+ let succeeded = 0;
205
+ let failed = 0;
206
+ let skipped = 0;
207
+
208
+ for (const game of games) {
209
+ console.log(`[${processed + 1}/${games.length}] ${game.game_id}`);
210
+
211
+ const result = await processGame(game);
212
+ processed++;
213
+
214
+ if (result.success) succeeded++;
215
+ else if (result.skipped) skipped++;
216
+ else failed++;
217
+
218
+ // Small delay to avoid rate limiting
219
+ await new Promise(r => setTimeout(r, 100));
220
+ }
221
+
222
+ console.log('');
223
+ console.log('šŸ“Š Summary');
224
+ console.log('==========');
225
+ console.log(` Total processed: ${processed}`);
226
+ console.log(` āœ… Succeeded: ${succeeded}`);
227
+ console.log(` āš ļø Skipped: ${skipped}`);
228
+ console.log(` āŒ Failed: ${failed}`);
229
+ console.log('');
230
+
231
+ process.exit(failed > 0 ? 1 : 0);
232
+
233
+ } catch (err) {
234
+ console.error('āŒ Fatal error:', err);
235
+ process.exit(1);
236
+ } finally {
237
+ await pool.end();
238
+ }
239
+ }
240
+
241
+ main().catch(err => {
242
+ console.error('āŒ Unhandled error:', err);
243
+ process.exit(1);
244
+ });
245
+
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Backfill missing my_signature values by querying Solana blockchain
4
+ *
5
+ * This script:
6
+ * 1. Finds user_game_refs records with NULL my_signature
7
+ * 2. Queries Solana for transactions from that wallet
8
+ * 3. Finds transactions that interact with the game PDA
9
+ * 4. Updates the database with the recovered signature
10
+ *
11
+ * Run with: node scripts/backfill-missing-signatures.js [--dry-run]
12
+ */
13
+
14
+ const { Pool } = require('pg');
15
+ const { Connection, PublicKey } = require('@solana/web3.js');
16
+ require('dotenv').config();
17
+
18
+ const pool = new Pool({
19
+ connectionString: process.env.DATABASE_URL,
20
+ });
21
+
22
+ // Use Alchemy or other RPC
23
+ const ALCHEMY_KEY = process.env.ALCHEMY_API_KEY || process.env.SOLANA_RPC_API_KEY;
24
+ const RPC_URL = ALCHEMY_KEY
25
+ ? `https://solana-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}`
26
+ : 'https://api.devnet.solana.com';
27
+
28
+ const connection = new Connection(RPC_URL, 'confirmed');
29
+
30
+ const isDryRun = process.argv.includes('--dry-run');
31
+
32
+ async function findJoinTransactionForGame(walletAddress, gameAddress) {
33
+ try {
34
+ const wallet = new PublicKey(walletAddress);
35
+ const gamePda = new PublicKey(gameAddress);
36
+
37
+ console.log(` šŸ” Searching for JOIN transaction...`);
38
+ console.log(` Wallet: ${walletAddress.slice(0, 8)}...`);
39
+ console.log(` Game PDA: ${gameAddress.slice(0, 8)}...`);
40
+
41
+ // Get signatures for the wallet (last 100 transactions)
42
+ const signatures = await connection.getSignaturesForAddress(wallet, {
43
+ limit: 100,
44
+ });
45
+
46
+ console.log(` Found ${signatures.length} wallet transactions to check`);
47
+
48
+ // Check each transaction to see if it involves the game PDA
49
+ for (const sigInfo of signatures) {
50
+ try {
51
+ const tx = await connection.getTransaction(sigInfo.signature, {
52
+ maxSupportedTransactionVersion: 0,
53
+ });
54
+
55
+ if (!tx || !tx.transaction) continue;
56
+
57
+ // Get the account keys from the transaction
58
+ const accountKeys = tx.transaction.message.staticAccountKeys ||
59
+ tx.transaction.message.accountKeys || [];
60
+
61
+ // Check if the game PDA is in the transaction accounts
62
+ const hasGamePda = accountKeys.some(key =>
63
+ key.toString() === gameAddress
64
+ );
65
+
66
+ if (hasGamePda) {
67
+ console.log(` āœ… Found matching transaction: ${sigInfo.signature.slice(0, 16)}...`);
68
+ return sigInfo.signature;
69
+ }
70
+ } catch (txError) {
71
+ // Skip transactions we can't parse
72
+ continue;
73
+ }
74
+ }
75
+
76
+ console.log(` āŒ No matching transaction found`);
77
+ return null;
78
+
79
+ } catch (error) {
80
+ console.error(` Error searching for transaction:`, error.message);
81
+ return null;
82
+ }
83
+ }
84
+
85
+ async function backfillMissingSignatures() {
86
+ console.log('šŸ”§ Backfill Missing Signatures Script');
87
+ console.log('=====================================');
88
+ console.log(`Mode: ${isDryRun ? 'šŸƒ DRY RUN (no changes will be made)' : 'šŸ’¾ LIVE (will update database)'}`);
89
+ console.log(`RPC: ${RPC_URL.includes('alchemy') ? 'Alchemy' : 'Public RPC'}`);
90
+ console.log('');
91
+
92
+ try {
93
+ // Find records with missing signatures
94
+ const result = await pool.query(`
95
+ SELECT
96
+ ugr.wallet_address,
97
+ ugr.game_id,
98
+ ugr.role,
99
+ g.game_address,
100
+ g.buy_in,
101
+ ugr.joined_at
102
+ FROM user_game_refs ugr
103
+ LEFT JOIN games g ON ugr.game_id = g.game_id
104
+ WHERE (ugr.my_signature IS NULL OR ugr.my_signature = '')
105
+ AND g.game_address IS NOT NULL
106
+ ORDER BY ugr.joined_at DESC
107
+ `);
108
+
109
+ if (result.rows.length === 0) {
110
+ console.log('āœ… No records with missing signatures found!');
111
+ return;
112
+ }
113
+
114
+ console.log(`šŸ“‹ Found ${result.rows.length} records with missing signatures:\n`);
115
+
116
+ let recovered = 0;
117
+ let failed = 0;
118
+
119
+ for (const row of result.rows) {
120
+ console.log(`\nšŸ“ Processing: ${row.game_id}`);
121
+ console.log(` Wallet: ${row.wallet_address.slice(0, 8)}...`);
122
+ console.log(` Role: ${row.role}`);
123
+ console.log(` Joined: ${row.joined_at}`);
124
+
125
+ const signature = await findJoinTransactionForGame(
126
+ row.wallet_address,
127
+ row.game_address
128
+ );
129
+
130
+ if (signature) {
131
+ const explorerUrl = `https://explorer.solana.com/tx/${signature}?cluster=${
132
+ process.env.SOLANA_NETWORK || 'devnet'
133
+ }`;
134
+
135
+ if (isDryRun) {
136
+ console.log(` šŸƒ DRY RUN: Would update with signature: ${signature.slice(0, 20)}...`);
137
+ recovered++;
138
+ } else {
139
+ // Update the database
140
+ await pool.query(`
141
+ UPDATE user_game_refs
142
+ SET my_signature = $1, my_explorer_url = $2
143
+ WHERE wallet_address = $3 AND game_id = $4
144
+ `, [signature, explorerUrl, row.wallet_address, row.game_id]);
145
+
146
+ console.log(` šŸ’¾ Updated database with recovered signature`);
147
+ recovered++;
148
+ }
149
+ } else {
150
+ console.log(` āš ļø Could not recover signature (transaction may be too old or not found)`);
151
+ failed++;
152
+ }
153
+
154
+ // Rate limit to avoid RPC throttling (2 seconds between records)
155
+ await new Promise(resolve => setTimeout(resolve, 2000));
156
+ }
157
+
158
+ console.log('\n=====================================');
159
+ console.log('šŸ“Š Summary:');
160
+ console.log(` āœ… Recovered: ${recovered}`);
161
+ console.log(` āŒ Failed: ${failed}`);
162
+ console.log(` šŸ“‹ Total: ${result.rows.length}`);
163
+
164
+ if (isDryRun && recovered > 0) {
165
+ console.log('\nšŸ’” Run without --dry-run to apply changes');
166
+ }
167
+
168
+ } catch (error) {
169
+ console.error('āŒ Error:', error);
170
+ } finally {
171
+ await pool.end();
172
+ }
173
+ }
174
+
175
+ backfillMissingSignatures();
@@ -0,0 +1,202 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Backfill Missing Referral Earnings
4
+ *
5
+ * This script finds all resolved games where:
6
+ * - The game creator has a referrer
7
+ * - No referral_earnings record exists
8
+ *
9
+ * And creates the missing referral_earnings records.
10
+ *
11
+ * Usage:
12
+ * node scripts/backfill-referral-earnings.js --dry-run # Preview changes
13
+ * node scripts/backfill-referral-earnings.js # Execute backfill
14
+ *
15
+ * On Heroku:
16
+ * heroku run node scripts/backfill-referral-earnings.js --app dubs-server-prod --dry-run
17
+ * heroku run node scripts/backfill-referral-earnings.js --app dubs-server-prod
18
+ */
19
+
20
+ require('dotenv').config();
21
+ const { Pool } = require('pg');
22
+
23
+ // Parse command line arguments
24
+ const isDryRun = process.argv.includes('--dry-run');
25
+
26
+ // Configuration
27
+ const REFERRAL_COMMISSION_RATE = 0.01; // 1%
28
+ const LAMPORTS_PER_SOL = 1_000_000_000;
29
+
30
+ // Database connection
31
+ const pool = new Pool({
32
+ connectionString: process.env.DATABASE_URL,
33
+ ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false
34
+ });
35
+
36
+ async function findMissingReferralEarnings() {
37
+ console.log('šŸ” Finding resolved games with missing referral earnings...\n');
38
+
39
+ const result = await pool.query(`
40
+ SELECT
41
+ g.game_id,
42
+ g.game_type,
43
+ g.buy_in,
44
+ g.title,
45
+ g.created_at as game_created_at,
46
+ g.updated_at as game_resolved_at,
47
+ COALESCE(array_length(g.home_team_players, 1), 0) as home_count,
48
+ COALESCE(array_length(g.away_team_players, 1), 0) as away_count,
49
+ COALESCE(array_length(g.draw_team_players, 1), 0) as draw_count,
50
+ g.created_by as creator_wallet,
51
+ creator.id as creator_user_id,
52
+ creator.username as creator_username,
53
+ referrer.id as referrer_user_id,
54
+ referrer.wallet_address as referrer_wallet,
55
+ referrer.username as referrer_username
56
+ FROM games g
57
+ JOIN users creator ON g.created_by = creator.wallet_address
58
+ JOIN users referrer ON creator.referral_code = referrer.my_referral_code
59
+ WHERE g.is_resolved = true
60
+ AND NOT EXISTS (
61
+ SELECT 1 FROM referral_earnings
62
+ WHERE game_id = g.game_id
63
+ )
64
+ ORDER BY g.created_at ASC
65
+ `);
66
+
67
+ return result.rows;
68
+ }
69
+
70
+ async function backfillReferralEarning(game) {
71
+ const playerCount = game.home_count + game.away_count + game.draw_count;
72
+ const buyInSOL = parseFloat(game.buy_in) || 0;
73
+ const potSizeSOL = buyInSOL * playerCount;
74
+ const potSizeLamports = Math.floor(potSizeSOL * LAMPORTS_PER_SOL);
75
+ const commissionLamports = Math.floor(potSizeLamports * REFERRAL_COMMISSION_RATE);
76
+
77
+ if (commissionLamports <= 0) {
78
+ console.log(` ā­ļø Skipping ${game.game_id} - pot too small (${potSizeSOL} SOL)`);
79
+ return { skipped: true, reason: 'pot_too_small' };
80
+ }
81
+
82
+ console.log(` šŸ“ ${game.game_id}`);
83
+ console.log(` Creator: ${game.creator_username} (${game.creator_wallet.slice(0, 8)}...)`);
84
+ console.log(` Referrer: ${game.referrer_username} (${game.referrer_wallet.slice(0, 8)}...)`);
85
+ console.log(` Pot: ${potSizeSOL} SOL (${playerCount} players Ɨ ${buyInSOL} SOL)`);
86
+ console.log(` Commission: ${commissionLamports / LAMPORTS_PER_SOL} SOL (1%)`);
87
+
88
+ if (isDryRun) {
89
+ console.log(` šŸ”ø DRY RUN - would insert referral_earnings record\n`);
90
+ return { skipped: false, dryRun: true };
91
+ }
92
+
93
+ try {
94
+ const insertResult = await pool.query(`
95
+ INSERT INTO referral_earnings (
96
+ referrer_user_id,
97
+ referrer_wallet,
98
+ referee_user_id,
99
+ referee_wallet,
100
+ game_id,
101
+ game_type,
102
+ pot_size,
103
+ referee_buy_in,
104
+ referee_won,
105
+ referee_payout,
106
+ commission_rate,
107
+ commission_amount,
108
+ status,
109
+ paid_at,
110
+ payout_tx_signature,
111
+ notes
112
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
113
+ ON CONFLICT (referee_wallet, game_id) DO NOTHING
114
+ RETURNING id
115
+ `, [
116
+ game.referrer_user_id,
117
+ game.referrer_wallet,
118
+ game.creator_user_id,
119
+ game.creator_wallet,
120
+ game.game_id,
121
+ game.game_type || 'sports',
122
+ potSizeLamports,
123
+ potSizeLamports, // Using pot size as "buy-in" since commission is based on whole pot
124
+ true, // Game was resolved
125
+ 0,
126
+ REFERRAL_COMMISSION_RATE,
127
+ commissionLamports,
128
+ 'paid', // Already paid on-chain
129
+ game.game_resolved_at || new Date(),
130
+ null, // No tx signature available for backfilled records
131
+ 'Backfilled - on-chain payment occurred at game resolution'
132
+ ]);
133
+
134
+ if (insertResult.rows.length > 0) {
135
+ console.log(` āœ… Created referral_earnings record (ID: ${insertResult.rows[0].id})\n`);
136
+ return { skipped: false, inserted: true, id: insertResult.rows[0].id };
137
+ } else {
138
+ console.log(` āš ļø Record already exists (conflict)\n`);
139
+ return { skipped: true, reason: 'conflict' };
140
+ }
141
+ } catch (error) {
142
+ console.error(` āŒ Error: ${error.message}\n`);
143
+ return { skipped: true, reason: 'error', error: error.message };
144
+ }
145
+ }
146
+
147
+ async function main() {
148
+ console.log('═══════════════════════════════════════════════════════════════');
149
+ console.log(' Referral Earnings Backfill Script');
150
+ console.log(` Mode: ${isDryRun ? 'šŸ”ø DRY RUN (no changes will be made)' : 'šŸ”¹ LIVE (will insert records)'}`);
151
+ console.log('═══════════════════════════════════════════════════════════════\n');
152
+
153
+ try {
154
+ const missingGames = await findMissingReferralEarnings();
155
+
156
+ if (missingGames.length === 0) {
157
+ console.log('āœ… No missing referral earnings found. All resolved games with referrers have records.\n');
158
+ return;
159
+ }
160
+
161
+ console.log(`Found ${missingGames.length} resolved game(s) with referrers but missing referral_earnings\n`);
162
+ console.log('───────────────────────────────────────────────────────────────\n');
163
+
164
+ let inserted = 0;
165
+ let skipped = 0;
166
+ let totalCommissionLamports = 0;
167
+
168
+ for (const game of missingGames) {
169
+ const result = await backfillReferralEarning(game);
170
+
171
+ if (result.skipped) {
172
+ skipped++;
173
+ } else {
174
+ inserted++;
175
+ const playerCount = game.home_count + game.away_count + game.draw_count;
176
+ const potSizeLamports = Math.floor(parseFloat(game.buy_in) * playerCount * LAMPORTS_PER_SOL);
177
+ totalCommissionLamports += Math.floor(potSizeLamports * REFERRAL_COMMISSION_RATE);
178
+ }
179
+ }
180
+
181
+ console.log('───────────────────────────────────────────────────────────────\n');
182
+ console.log('šŸ“Š Summary:');
183
+ console.log(` Total games processed: ${missingGames.length}`);
184
+ console.log(` Records ${isDryRun ? 'to be inserted' : 'inserted'}: ${inserted}`);
185
+ console.log(` Skipped: ${skipped}`);
186
+ console.log(` Total commission: ${totalCommissionLamports / LAMPORTS_PER_SOL} SOL`);
187
+
188
+ if (isDryRun) {
189
+ console.log('\nšŸ”ø DRY RUN complete. Run without --dry-run to execute backfill.');
190
+ } else {
191
+ console.log('\nāœ… Backfill complete!');
192
+ }
193
+
194
+ } catch (error) {
195
+ console.error('āŒ Fatal error:', error);
196
+ process.exit(1);
197
+ } finally {
198
+ await pool.end();
199
+ }
200
+ }
201
+
202
+ main();