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,355 @@
1
+ /**
2
+ * Keeper State Service
3
+ *
4
+ * Manages keeper state in PostgreSQL for:
5
+ * - Round lifecycle tracking
6
+ * - Recovery after crashes
7
+ * - Multi-keeper coordination
8
+ * - Monitoring and alerting
9
+ *
10
+ * NOTE: Gracefully degrades to no-op when DATABASE_URL is not set (local dev)
11
+ */
12
+
13
+ const { pool } = require('./db'); // Shared database pool
14
+
15
+ class KeeperStateService {
16
+ constructor() {
17
+ // Check if database is configured
18
+ this.enabled = !!process.env.DATABASE_URL;
19
+
20
+ if (this.enabled) {
21
+ // Use shared pool from services/db.js
22
+ this.pool = pool;
23
+ console.log('📊 Keeper state service: Database enabled');
24
+ } else {
25
+ console.log('⚠️ Keeper state service: No DATABASE_URL set - running without state persistence (local dev mode)');
26
+ this.pool = null;
27
+ }
28
+ }
29
+
30
+ // Helper to check if DB operations should run
31
+ _shouldRun() {
32
+ return this.enabled && this.pool;
33
+ }
34
+
35
+ /**
36
+ * Initialize a new round in the database
37
+ */
38
+ async createRound(roundId, initialStatus = 'open') {
39
+ if (!this._shouldRun()) return null;
40
+
41
+ try {
42
+ const result = await this.pool.query(
43
+ `INSERT INTO keeper_rounds (round_id, status)
44
+ VALUES ($1, $2)
45
+ ON CONFLICT (round_id) DO NOTHING
46
+ RETURNING *`,
47
+ [roundId, initialStatus]
48
+ );
49
+ console.log(`📝 Created round ${roundId} in DB with status: ${initialStatus}`);
50
+ return result.rows[0];
51
+ } catch (error) {
52
+ console.error(`❌ Failed to create round ${roundId}:`, error.message);
53
+ return null; // Don't throw - allow keeper to continue
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Get round state from database
59
+ */
60
+ async getRound(roundId) {
61
+ if (!this._shouldRun()) return null;
62
+
63
+ try {
64
+ const result = await this.pool.query(
65
+ 'SELECT * FROM keeper_rounds WHERE round_id = $1',
66
+ [roundId]
67
+ );
68
+ return result.rows[0] || null;
69
+ } catch (error) {
70
+ console.error(`❌ Failed to get round ${roundId}:`, error.message);
71
+ return null;
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Update round status and metadata
77
+ */
78
+ async updateRound(roundId, updates) {
79
+ if (!this._shouldRun()) return null;
80
+
81
+ try {
82
+ const fields = [];
83
+ const values = [roundId];
84
+ let paramIndex = 2;
85
+
86
+ // Build dynamic UPDATE query
87
+ if (updates.status) {
88
+ fields.push(`status = $${paramIndex++}`);
89
+ values.push(updates.status);
90
+ }
91
+ if (updates.locked_at) {
92
+ fields.push(`locked_at = $${paramIndex++}`);
93
+ values.push(updates.locked_at);
94
+ }
95
+ if (updates.revealed_at) {
96
+ fields.push(`revealed_at = $${paramIndex++}`);
97
+ values.push(updates.revealed_at);
98
+ }
99
+ if (updates.resolved_at) {
100
+ fields.push(`resolved_at = $${paramIndex++}`);
101
+ values.push(updates.resolved_at);
102
+ }
103
+ if (updates.reset_at) {
104
+ fields.push(`reset_at = $${paramIndex++}`);
105
+ values.push(updates.reset_at);
106
+ }
107
+ if (updates.retry_count !== undefined) {
108
+ fields.push(`retry_count = $${paramIndex++}`);
109
+ values.push(updates.retry_count);
110
+ }
111
+ if (updates.last_error !== undefined) {
112
+ fields.push(`last_error = $${paramIndex++}`);
113
+ values.push(updates.last_error);
114
+ }
115
+ if (updates.last_attempt_at) {
116
+ fields.push(`last_attempt_at = $${paramIndex++}`);
117
+ values.push(updates.last_attempt_at);
118
+ }
119
+ if (updates.total_pot !== undefined) {
120
+ fields.push(`total_pot = $${paramIndex++}`);
121
+ values.push(updates.total_pot);
122
+ }
123
+ if (updates.entry_count !== undefined) {
124
+ fields.push(`entry_count = $${paramIndex++}`);
125
+ values.push(updates.entry_count);
126
+ }
127
+ if (updates.winner_pubkey) {
128
+ fields.push(`winner_pubkey = $${paramIndex++}`);
129
+ values.push(updates.winner_pubkey);
130
+ }
131
+ if (updates.win_amount !== undefined) {
132
+ fields.push(`win_amount = $${paramIndex++}`);
133
+ values.push(updates.win_amount);
134
+ }
135
+ if (updates.lock_signature) {
136
+ fields.push(`lock_signature = $${paramIndex++}`);
137
+ values.push(updates.lock_signature);
138
+ }
139
+ if (updates.reveal_signature) {
140
+ fields.push(`reveal_signature = $${paramIndex++}`);
141
+ values.push(updates.reveal_signature);
142
+ }
143
+ if (updates.resolve_signature) {
144
+ fields.push(`resolve_signature = $${paramIndex++}`);
145
+ values.push(updates.resolve_signature);
146
+ }
147
+ if (updates.reset_signature) {
148
+ fields.push(`reset_signature = $${paramIndex++}`);
149
+ values.push(updates.reset_signature);
150
+ }
151
+
152
+ if (fields.length === 0) {
153
+ console.warn(`⚠️ No fields to update for round ${roundId}`);
154
+ return null;
155
+ }
156
+
157
+ const query = `
158
+ UPDATE keeper_rounds
159
+ SET ${fields.join(', ')}
160
+ WHERE round_id = $1
161
+ RETURNING *
162
+ `;
163
+
164
+ const result = await this.pool.query(query, values);
165
+ return result.rows[0];
166
+ } catch (error) {
167
+ console.error(`❌ Failed to update round ${roundId}:`, error.message);
168
+ return null; // Don't throw - allow keeper to continue
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Log a keeper action for debugging
174
+ */
175
+ async logAction(roundId, action, success, errorMessage = null, signature = null, durationMs = null) {
176
+ if (!this._shouldRun()) return;
177
+
178
+ try {
179
+ await this.pool.query(
180
+ `INSERT INTO keeper_actions (round_id, action, success, error_message, signature, duration_ms)
181
+ VALUES ($1, $2, $3, $4, $5, $6)`,
182
+ [roundId, action, success, errorMessage, signature, durationMs]
183
+ );
184
+ } catch (error) {
185
+ // Don't throw - logging failures shouldn't break the keeper
186
+ console.error(`⚠️ Failed to log action:`, error.message);
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Find rounds that are stuck (not completed after 5 minutes)
192
+ */
193
+ async getStuckRounds() {
194
+ if (!this._shouldRun()) return [];
195
+
196
+ try {
197
+ const result = await this.pool.query(
198
+ `SELECT * FROM stuck_rounds`
199
+ );
200
+ return result.rows;
201
+ } catch (error) {
202
+ console.error(`❌ Failed to get stuck rounds:`, error.message);
203
+ return [];
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Get the current round that needs processing
209
+ * Returns the round with highest round_id that's not resolved
210
+ */
211
+ async getCurrentActiveRound() {
212
+ if (!this._shouldRun()) return null;
213
+
214
+ try {
215
+ const result = await this.pool.query(
216
+ `SELECT * FROM keeper_rounds
217
+ WHERE status != 'resolved'
218
+ ORDER BY round_id DESC
219
+ LIMIT 1`
220
+ );
221
+ return result.rows[0] || null;
222
+ } catch (error) {
223
+ console.error(`❌ Failed to get active round:`, error.message);
224
+ return null;
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Increment retry count for a round
230
+ */
231
+ async incrementRetry(roundId, errorMessage) {
232
+ if (!this._shouldRun()) return;
233
+
234
+ try {
235
+ await this.pool.query(
236
+ `UPDATE keeper_rounds
237
+ SET retry_count = retry_count + 1,
238
+ last_error = $2,
239
+ last_attempt_at = NOW()
240
+ WHERE round_id = $1`,
241
+ [roundId, errorMessage]
242
+ );
243
+ } catch (error) {
244
+ console.error(`❌ Failed to increment retry:`, error.message);
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Get health summary
250
+ */
251
+ async getHealthSummary() {
252
+ if (!this._shouldRun()) return null;
253
+
254
+ try {
255
+ const result = await this.pool.query('SELECT * FROM keeper_health_summary');
256
+ return result.rows[0];
257
+ } catch (error) {
258
+ console.error(`❌ Failed to get health summary:`, error.message);
259
+ return null;
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Record health check
265
+ */
266
+ async recordHealth(metrics) {
267
+ if (!this._shouldRun()) return;
268
+
269
+ try {
270
+ await this.pool.query(
271
+ `INSERT INTO keeper_health (rounds_completed, consecutive_failures, last_success_round, last_error, uptime_seconds)
272
+ VALUES ($1, $2, $3, $4, $5)`,
273
+ [
274
+ metrics.roundsCompleted,
275
+ metrics.consecutiveFailures,
276
+ metrics.lastSuccessRound,
277
+ metrics.lastError,
278
+ metrics.uptimeSeconds
279
+ ]
280
+ );
281
+ } catch (error) {
282
+ console.error(`⚠️ Failed to record health:`, error.message);
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Clean up old action logs (keep last 1000 or last 7 days)
288
+ */
289
+ async cleanupOldLogs() {
290
+ if (!this._shouldRun()) return;
291
+
292
+ try {
293
+ await this.pool.query(
294
+ `DELETE FROM keeper_actions
295
+ WHERE id NOT IN (
296
+ SELECT id FROM keeper_actions
297
+ ORDER BY timestamp DESC
298
+ LIMIT 1000
299
+ )
300
+ AND timestamp < NOW() - INTERVAL '7 days'`
301
+ );
302
+ } catch (error) {
303
+ console.error(`⚠️ Failed to cleanup logs:`, error.message);
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Get recent actions for a round
309
+ */
310
+ async getRoundActions(roundId, limit = 20) {
311
+ if (!this._shouldRun()) return [];
312
+
313
+ try {
314
+ const result = await this.pool.query(
315
+ `SELECT * FROM keeper_actions
316
+ WHERE round_id = $1
317
+ ORDER BY timestamp DESC
318
+ LIMIT $2`,
319
+ [roundId, limit]
320
+ );
321
+ return result.rows;
322
+ } catch (error) {
323
+ console.error(`❌ Failed to get round actions:`, error.message);
324
+ return [];
325
+ }
326
+ }
327
+
328
+ /**
329
+ * Check if round is stuck (based on status and last update time)
330
+ */
331
+ isRoundStuck(dbRound) {
332
+ if (!dbRound) return false;
333
+
334
+ const stuckStatuses = ['locking', 'revealing', 'resolving', 'resetting'];
335
+ if (!stuckStatuses.includes(dbRound.status)) return false;
336
+
337
+ const updatedAt = new Date(dbRound.updated_at);
338
+ const now = new Date();
339
+ const minutesStuck = (now - updatedAt) / 1000 / 60;
340
+
341
+ return minutesStuck > 5; // Stuck if no update in 5 minutes
342
+ }
343
+
344
+ /**
345
+ * Close database connection
346
+ */
347
+ async close() {
348
+ if (this.pool) {
349
+ await this.pool.end();
350
+ }
351
+ }
352
+ }
353
+
354
+ module.exports = KeeperStateService;
355
+