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,389 @@
1
+ /**
2
+ * 📊 Player Stats Service
3
+ *
4
+ * Tracks player PNL, win/loss history, and provides chart data
5
+ */
6
+
7
+ const { pool } = require('./db'); // Shared database pool
8
+ const { LAMPORTS_PER_SOL } = require('@solana/web3.js');
9
+
10
+ class PlayerStatsService {
11
+ constructor() {
12
+ // Use shared pool from services/db.js
13
+ this.pool = pool;
14
+
15
+ this.initializeTables();
16
+ }
17
+
18
+ async initializeTables() {
19
+ try {
20
+ // Player stats table - overall performance with streak tracking
21
+ await this.pool.query(`
22
+ CREATE TABLE IF NOT EXISTS player_stats (
23
+ wallet_address VARCHAR(100) PRIMARY KEY,
24
+ total_wagered NUMERIC(20, 9) DEFAULT 0,
25
+ total_won NUMERIC(20, 9) DEFAULT 0,
26
+ net_pnl NUMERIC(20, 9) DEFAULT 0,
27
+ rounds_played INTEGER DEFAULT 0,
28
+ rounds_won INTEGER DEFAULT 0,
29
+ biggest_win NUMERIC(20, 9) DEFAULT 0,
30
+ biggest_win_round INTEGER,
31
+ current_win_streak INTEGER DEFAULT 0,
32
+ longest_win_streak INTEGER DEFAULT 0,
33
+ last_win_round INTEGER,
34
+ last_played TIMESTAMP,
35
+ created_at TIMESTAMP DEFAULT NOW()
36
+ );
37
+
38
+ CREATE INDEX IF NOT EXISTS idx_player_stats_pnl ON player_stats(net_pnl DESC);
39
+ CREATE INDEX IF NOT EXISTS idx_player_stats_wagered ON player_stats(total_wagered DESC);
40
+ CREATE INDEX IF NOT EXISTS idx_player_stats_streak ON player_stats(current_win_streak DESC);
41
+ `);
42
+
43
+ // Add streak columns if they don't exist (migration for existing tables)
44
+ await this.pool.query(`
45
+ DO $$
46
+ BEGIN
47
+ IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'player_stats' AND column_name = 'current_win_streak') THEN
48
+ ALTER TABLE player_stats ADD COLUMN current_win_streak INTEGER DEFAULT 0;
49
+ END IF;
50
+ IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'player_stats' AND column_name = 'longest_win_streak') THEN
51
+ ALTER TABLE player_stats ADD COLUMN longest_win_streak INTEGER DEFAULT 0;
52
+ END IF;
53
+ IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'player_stats' AND column_name = 'last_win_round') THEN
54
+ ALTER TABLE player_stats ADD COLUMN last_win_round INTEGER;
55
+ END IF;
56
+ END $$;
57
+ `);
58
+
59
+ // Player history table - for chart data
60
+ await this.pool.query(`
61
+ CREATE TABLE IF NOT EXISTS player_history (
62
+ id SERIAL PRIMARY KEY,
63
+ wallet_address VARCHAR(100) NOT NULL,
64
+ round_id INTEGER NOT NULL,
65
+ action VARCHAR(20) NOT NULL,
66
+ amount NUMERIC(20, 9) NOT NULL,
67
+ cumulative_pnl NUMERIC(20, 9) NOT NULL,
68
+ timestamp TIMESTAMP DEFAULT NOW()
69
+ );
70
+
71
+ CREATE INDEX IF NOT EXISTS idx_player_history_wallet ON player_history(wallet_address, timestamp DESC);
72
+ CREATE INDEX IF NOT EXISTS idx_player_history_round ON player_history(round_id);
73
+ `);
74
+
75
+ console.log('✅ Player stats tables initialized');
76
+ } catch (error) {
77
+ console.error('❌ Failed to initialize player stats tables:', error.message);
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Record a player entry (bet)
83
+ */
84
+ async recordEntry({ walletAddress, roundId, amount }) {
85
+ try {
86
+ const amountSOL = amount / LAMPORTS_PER_SOL;
87
+
88
+ // Update or insert player stats
89
+ await this.pool.query(`
90
+ INSERT INTO player_stats (
91
+ wallet_address,
92
+ total_wagered,
93
+ rounds_played,
94
+ net_pnl,
95
+ last_played
96
+ )
97
+ VALUES ($1, $2::NUMERIC, 1, (0 - $2::NUMERIC), NOW())
98
+ ON CONFLICT (wallet_address) DO UPDATE SET
99
+ total_wagered = player_stats.total_wagered + $2::NUMERIC,
100
+ rounds_played = player_stats.rounds_played + 1,
101
+ net_pnl = player_stats.net_pnl - $2::NUMERIC,
102
+ last_played = NOW()
103
+ `, [walletAddress, amountSOL]);
104
+
105
+ // Get current cumulative PNL
106
+ const result = await this.pool.query(
107
+ 'SELECT net_pnl FROM player_stats WHERE wallet_address = $1',
108
+ [walletAddress]
109
+ );
110
+ const cumulativePNL = parseFloat(result.rows[0].net_pnl);
111
+
112
+ // Add to history for chart
113
+ await this.pool.query(`
114
+ INSERT INTO player_history (
115
+ wallet_address,
116
+ round_id,
117
+ action,
118
+ amount,
119
+ cumulative_pnl
120
+ )
121
+ VALUES ($1, $2, 'entry', $3, $4)
122
+ `, [walletAddress, roundId, amountSOL, cumulativePNL]);
123
+
124
+ console.log(`📊 Entry recorded: ${walletAddress.slice(0, 8)}... wagered ◎${amountSOL}`);
125
+ } catch (error) {
126
+ console.error('Error recording entry:', error);
127
+ throw error;
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Record a player win with streak tracking
133
+ */
134
+ async recordWin({ walletAddress, roundId, winAmount }) {
135
+ try {
136
+ const winSOL = winAmount / LAMPORTS_PER_SOL;
137
+ const roundIdNum = parseInt(roundId);
138
+
139
+ // First, get current player stats to calculate streak
140
+ const existingStats = await this.pool.query(
141
+ 'SELECT current_win_streak, last_win_round FROM player_stats WHERE wallet_address = $1',
142
+ [walletAddress]
143
+ );
144
+
145
+ let newStreak = 1;
146
+ if (existingStats.rows.length > 0) {
147
+ const { current_win_streak, last_win_round } = existingStats.rows[0];
148
+ // If they won the previous round, continue streak; otherwise reset to 1
149
+ if (last_win_round && roundIdNum === last_win_round + 1) {
150
+ newStreak = (current_win_streak || 0) + 1;
151
+ }
152
+ }
153
+
154
+ // Update player stats with streak tracking
155
+ await this.pool.query(`
156
+ INSERT INTO player_stats (
157
+ wallet_address,
158
+ total_won,
159
+ rounds_won,
160
+ net_pnl,
161
+ biggest_win,
162
+ biggest_win_round,
163
+ current_win_streak,
164
+ longest_win_streak,
165
+ last_win_round,
166
+ last_played
167
+ )
168
+ VALUES ($1, $2, 1, $2, $2, $3, $4, $4, $3, NOW())
169
+ ON CONFLICT (wallet_address) DO UPDATE SET
170
+ total_won = player_stats.total_won + $2,
171
+ rounds_won = player_stats.rounds_won + 1,
172
+ net_pnl = player_stats.net_pnl + $2,
173
+ biggest_win = GREATEST(player_stats.biggest_win, $2),
174
+ biggest_win_round = CASE
175
+ WHEN $2 > player_stats.biggest_win THEN $3
176
+ ELSE player_stats.biggest_win_round
177
+ END,
178
+ current_win_streak = $4,
179
+ longest_win_streak = GREATEST(COALESCE(player_stats.longest_win_streak, 0), $4),
180
+ last_win_round = $3,
181
+ last_played = NOW()
182
+ `, [walletAddress, winSOL, roundIdNum, newStreak]);
183
+
184
+ // Get current cumulative PNL
185
+ const result = await this.pool.query(
186
+ 'SELECT net_pnl FROM player_stats WHERE wallet_address = $1',
187
+ [walletAddress]
188
+ );
189
+ const cumulativePNL = parseFloat(result.rows[0].net_pnl);
190
+
191
+ // Add to history for chart
192
+ await this.pool.query(`
193
+ INSERT INTO player_history (
194
+ wallet_address,
195
+ round_id,
196
+ action,
197
+ amount,
198
+ cumulative_pnl
199
+ )
200
+ VALUES ($1, $2, 'win', $3, $4)
201
+ `, [walletAddress, roundIdNum, winSOL, cumulativePNL]);
202
+
203
+ console.log(`🏆 Win recorded: ${walletAddress.slice(0, 8)}... won ◎${winSOL} (streak: ${newStreak}🔥)`);
204
+
205
+ return { newStreak };
206
+ } catch (error) {
207
+ console.error('Error recording win:', error);
208
+ throw error;
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Get player stats
214
+ */
215
+ async getPlayerStats(walletAddress) {
216
+ try {
217
+ const result = await this.pool.query(
218
+ 'SELECT * FROM player_stats WHERE wallet_address = $1',
219
+ [walletAddress]
220
+ );
221
+
222
+ if (result.rows.length === 0) {
223
+ return null;
224
+ }
225
+
226
+ const stats = result.rows[0];
227
+ return {
228
+ walletAddress: stats.wallet_address,
229
+ totalWagered: parseFloat(stats.total_wagered),
230
+ totalWon: parseFloat(stats.total_won),
231
+ netPNL: parseFloat(stats.net_pnl),
232
+ roundsPlayed: stats.rounds_played,
233
+ roundsWon: stats.rounds_won,
234
+ winRate: stats.rounds_played > 0 ? (stats.rounds_won / stats.rounds_played * 100).toFixed(2) : 0,
235
+ biggestWin: parseFloat(stats.biggest_win),
236
+ biggestWinRound: stats.biggest_win_round,
237
+ currentWinStreak: stats.current_win_streak || 0,
238
+ longestWinStreak: stats.longest_win_streak || 0,
239
+ lastWinRound: stats.last_win_round,
240
+ lastPlayed: stats.last_played,
241
+ createdAt: stats.created_at,
242
+ };
243
+ } catch (error) {
244
+ console.error('Error getting player stats:', error);
245
+ return null;
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Get the luckiest player (highest current winning streak)
251
+ */
252
+ async getLuckiestPlayer() {
253
+ try {
254
+ const result = await this.pool.query(`
255
+ SELECT
256
+ wallet_address,
257
+ current_win_streak,
258
+ longest_win_streak,
259
+ rounds_won,
260
+ total_won,
261
+ last_win_round
262
+ FROM player_stats
263
+ WHERE current_win_streak > 0
264
+ ORDER BY current_win_streak DESC, total_won DESC
265
+ LIMIT 1
266
+ `);
267
+
268
+ if (result.rows.length === 0) {
269
+ return null;
270
+ }
271
+
272
+ const stats = result.rows[0];
273
+ return {
274
+ walletAddress: stats.wallet_address,
275
+ currentStreak: stats.current_win_streak,
276
+ longestStreak: stats.longest_win_streak,
277
+ roundsWon: stats.rounds_won,
278
+ totalWon: parseFloat(stats.total_won),
279
+ lastWinRound: stats.last_win_round,
280
+ };
281
+ } catch (error) {
282
+ console.error('Error getting luckiest player:', error);
283
+ return null;
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Get top streakers (leaderboard by current streak)
289
+ */
290
+ async getStreakLeaderboard(limit = 10) {
291
+ try {
292
+ const result = await this.pool.query(`
293
+ SELECT
294
+ wallet_address,
295
+ current_win_streak,
296
+ longest_win_streak,
297
+ rounds_won,
298
+ total_won
299
+ FROM player_stats
300
+ WHERE current_win_streak > 0
301
+ ORDER BY current_win_streak DESC, total_won DESC
302
+ LIMIT $1
303
+ `, [limit]);
304
+
305
+ return result.rows.map(stats => ({
306
+ walletAddress: stats.wallet_address,
307
+ currentStreak: stats.current_win_streak,
308
+ longestStreak: stats.longest_win_streak,
309
+ roundsWon: stats.rounds_won,
310
+ totalWon: parseFloat(stats.total_won),
311
+ }));
312
+ } catch (error) {
313
+ console.error('Error getting streak leaderboard:', error);
314
+ return [];
315
+ }
316
+ }
317
+
318
+ /**
319
+ * Get player history for chart
320
+ */
321
+ async getPlayerHistory(walletAddress, limit = 100) {
322
+ try {
323
+ const result = await this.pool.query(
324
+ `SELECT * FROM player_history
325
+ WHERE wallet_address = $1
326
+ ORDER BY timestamp ASC
327
+ LIMIT $2`,
328
+ [walletAddress, limit]
329
+ );
330
+
331
+ return result.rows.map(row => ({
332
+ id: row.id,
333
+ roundId: row.round_id,
334
+ action: row.action,
335
+ amount: parseFloat(row.amount),
336
+ cumulativePNL: parseFloat(row.cumulative_pnl),
337
+ timestamp: row.timestamp,
338
+ }));
339
+ } catch (error) {
340
+ console.error('Error getting player history:', error);
341
+ return [];
342
+ }
343
+ }
344
+
345
+ /**
346
+ * Get leaderboard (top players by PNL)
347
+ */
348
+ async getLeaderboard(limit = 10) {
349
+ try {
350
+ const result = await this.pool.query(
351
+ `SELECT * FROM player_stats
352
+ ORDER BY net_pnl DESC
353
+ LIMIT $1`,
354
+ [limit]
355
+ );
356
+
357
+ return result.rows.map(stats => ({
358
+ walletAddress: stats.wallet_address,
359
+ netPNL: parseFloat(stats.net_pnl),
360
+ totalWagered: parseFloat(stats.total_wagered),
361
+ totalWon: parseFloat(stats.total_won),
362
+ roundsPlayed: stats.rounds_played,
363
+ roundsWon: stats.rounds_won,
364
+ winRate: stats.rounds_played > 0 ? (stats.rounds_won / stats.rounds_played * 100).toFixed(2) : 0,
365
+ }));
366
+ } catch (error) {
367
+ console.error('Error getting leaderboard:', error);
368
+ return [];
369
+ }
370
+ }
371
+
372
+ /**
373
+ * Cleanup old history (keep last 30 days)
374
+ */
375
+ async cleanup() {
376
+ try {
377
+ await this.pool.query(`
378
+ DELETE FROM player_history
379
+ WHERE timestamp < NOW() - INTERVAL '30 days'
380
+ `);
381
+ console.log('🧹 Cleaned up old player history (30+ days)');
382
+ } catch (error) {
383
+ console.error('Error cleaning up player history:', error);
384
+ }
385
+ }
386
+ }
387
+
388
+ module.exports = PlayerStatsService;
389
+