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,742 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * šŸ¤– Complete Jackpot Keeper Bot
5
+ *
6
+ * Manages full round lifecycle:
7
+ * 1. Lock round when timer expires
8
+ * 2. Submit oracle randomness
9
+ * 3. Resolve and pay winner
10
+ * 4. Open new round
11
+ */
12
+
13
+ const { Connection, Keypair, Transaction, PublicKey } = require('@solana/web3.js');
14
+ const axios = require('axios');
15
+ const crypto = require('crypto');
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const { instance: historyInstance } = require('../../services/jackpotHistory');
19
+ const KeeperStateService = require('../../services/keeperStateService');
20
+
21
+ const RPC_URL = process.env.SOLANA_RPC_URL || process.env.SOLANA_NETWORK || 'https://api.devnet.solana.com';
22
+ const API_BASE = process.env.API_BASE_URL || 'http://localhost:3001';
23
+
24
+ class JackpotKeeper {
25
+ constructor(io = null) {
26
+ this.connection = new Connection(RPC_URL, 'confirmed');
27
+ this.history = historyInstance; // Use shared singleton instance
28
+ this.state = new KeeperStateService();
29
+ this.programId = new PublicKey(process.env.JACKPOT_PROGRAM_ID || 'BHidyz25KWkNPdTHgeANzMg25MM2KEiNnG4yE5F46XUz');
30
+ this.startTime = Date.now();
31
+ this.roundsCompleted = 0;
32
+ this.consecutiveFailures = 0;
33
+ this.io = io; // WebSocket server instance
34
+ this.lastEntryCount = {}; // Track entry count per round for change detection
35
+ this.vrfDataPath = path.join(__dirname, '..', '..', '.vrf-data.json'); // Persists VRF seeds across restarts
36
+
37
+ // Load wallet - Priority: ENV var > oracle wallet file > main wallet file
38
+ let secretKey;
39
+
40
+ if (process.env.KEEPER_PRIVATE_KEY) {
41
+ // For Heroku - use environment variable
42
+ console.log('šŸ“‚ Using wallet from KEEPER_PRIVATE_KEY environment variable');
43
+ secretKey = JSON.parse(process.env.KEEPER_PRIVATE_KEY);
44
+ } else {
45
+ // For local development - use wallet files
46
+ let walletPath;
47
+ const oracleWalletPath = path.join(__dirname, '..', '..', 'wallets', 'jackpot_oracle.json');
48
+ const mainWalletPath = path.join(require('os').homedir(), '.config/solana/id.json');
49
+
50
+ if (fs.existsSync(oracleWalletPath)) {
51
+ walletPath = oracleWalletPath;
52
+ console.log('šŸ“‚ Using oracle wallet from wallets directory');
53
+ } else if (fs.existsSync(mainWalletPath)) {
54
+ walletPath = mainWalletPath;
55
+ console.log('šŸ“‚ Using main wallet from home directory');
56
+ } else {
57
+ throw new Error('āŒ No wallet found! Set KEEPER_PRIVATE_KEY env var or add wallet file');
58
+ }
59
+
60
+ secretKey = JSON.parse(fs.readFileSync(walletPath, 'utf-8'));
61
+ }
62
+
63
+ this.wallet = Keypair.fromSecretKey(Uint8Array.from(secretKey));
64
+
65
+ console.log('šŸ¤– Complete Jackpot Keeper Bot');
66
+ console.log(' RPC:', RPC_URL.includes('devnet') ? 'Devnet' : RPC_URL.slice(0, 40) + '...');
67
+ console.log(' Program:', this.programId.toString());
68
+ console.log(' Wallet:', this.wallet.publicKey.toString());
69
+ console.log();
70
+ }
71
+
72
+ // Persist VRF data to disk so it survives keeper restarts
73
+ _saveVrfData() {
74
+ try {
75
+ if (this.currentRoundData) {
76
+ fs.writeFileSync(this.vrfDataPath, JSON.stringify(this.currentRoundData));
77
+ }
78
+ } catch (e) {
79
+ console.warn('āš ļø Could not persist VRF data:', e.message);
80
+ }
81
+ }
82
+
83
+ _loadVrfData() {
84
+ try {
85
+ if (fs.existsSync(this.vrfDataPath)) {
86
+ return JSON.parse(fs.readFileSync(this.vrfDataPath, 'utf-8'));
87
+ }
88
+ } catch (e) {
89
+ console.warn('āš ļø Could not load VRF data:', e.message);
90
+ }
91
+ return null;
92
+ }
93
+
94
+ _clearVrfData() {
95
+ try {
96
+ if (fs.existsSync(this.vrfDataPath)) {
97
+ fs.unlinkSync(this.vrfDataPath);
98
+ }
99
+ } catch (e) { /* ignore */ }
100
+ }
101
+
102
+ // Polling-based tx confirmation (Alchemy doesn't support signatureSubscribe websocket)
103
+ async confirmTx(signature, timeout = 60000) {
104
+ const start = Date.now();
105
+ while (Date.now() - start < timeout) {
106
+ const statuses = await this.connection.getSignatureStatuses([signature]);
107
+ const status = statuses?.value?.[0];
108
+ if (status?.err) {
109
+ throw new Error(`Transaction failed: ${JSON.stringify(status.err)}`);
110
+ }
111
+ if (status?.confirmationStatus === 'confirmed' || status?.confirmationStatus === 'finalized') {
112
+ return;
113
+ }
114
+ await new Promise(resolve => setTimeout(resolve, 2000));
115
+ }
116
+ throw new Error('Transaction confirmation timeout');
117
+ }
118
+
119
+ async getCurrentRound() {
120
+ try {
121
+ const { data } = await axios.get(`${API_BASE}/jackpot/round/current`);
122
+
123
+ // ALSO fetch entries to get REAL entry count
124
+ if (data.round) {
125
+ const entriesRes = await axios.get(`${API_BASE}/jackpot/round/${data.round.roundId}/entries`);
126
+ data.round.entryCount = entriesRes.data.entries?.length || 0;
127
+ }
128
+
129
+ return data.round;
130
+ } catch (error) {
131
+ return null;
132
+ }
133
+ }
134
+
135
+ async lockRound(roundId) {
136
+ console.log(`šŸ”’ Locking round ${roundId}...`);
137
+ const startTime = Date.now();
138
+
139
+ try {
140
+ // Update state: locking
141
+ await this.state.updateRound(roundId, {
142
+ status: 'locking',
143
+ last_attempt_at: new Date()
144
+ });
145
+
146
+ const { data } = await axios.post(`${API_BASE}/jackpot/build/lock-round`, {
147
+ keeperAddress: this.wallet.publicKey.toString(),
148
+ roundId,
149
+ });
150
+
151
+ const tx = Transaction.from(Buffer.from(data.transaction, 'base64'));
152
+ tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash;
153
+ tx.feePayer = this.wallet.publicKey;
154
+ tx.sign(this.wallet);
155
+
156
+ const signature = await this.connection.sendRawTransaction(tx.serialize(), {
157
+ skipPreflight: true,
158
+ });
159
+ await this.confirmTx(signature);
160
+
161
+ const duration = Date.now() - startTime;
162
+
163
+ // Update state: locked
164
+ await this.state.updateRound(roundId, {
165
+ status: 'locked',
166
+ locked_at: new Date(),
167
+ lock_signature: signature
168
+ });
169
+
170
+ // Log action
171
+ await this.state.logAction(roundId, 'lock', true, null, signature, duration);
172
+
173
+ // Store server seed data for verification later (memory + disk)
174
+ this.currentRoundData = {
175
+ serverSeed: data.serverSeed,
176
+ serverSeedHash: data.serverSeedHash,
177
+ };
178
+ this._saveVrfData();
179
+
180
+ console.log(`āœ… Round ${roundId} locked! Sig: ${signature.slice(0, 8)}...`);
181
+ console.log(`šŸ“ Server seed hash: ${data.serverSeedHash?.slice(0, 16)}...`);
182
+
183
+ // Trigger WebSocket broadcast via webhook
184
+ axios.post(`${API_BASE}/api/keeper-webhook/round-locked`, {
185
+ roundId
186
+ }).catch(err => console.warn('Webhook broadcast failed:', err.message));
187
+
188
+ return true;
189
+ } catch (error) {
190
+ console.error('āŒ Lock failed:', error.message);
191
+
192
+ // Log failure
193
+ await this.state.incrementRetry(roundId, error.message);
194
+ await this.state.logAction(roundId, 'lock', false, error.message, null, Date.now() - startTime);
195
+
196
+ return false;
197
+ }
198
+ }
199
+
200
+ async revealRandomness(roundId) {
201
+ console.log(`šŸŽ² Revealing randomness for round ${roundId}...`);
202
+ const startTime = Date.now();
203
+
204
+ try {
205
+ // Update state: revealing
206
+ await this.state.updateRound(roundId, {
207
+ status: 'revealing',
208
+ last_attempt_at: new Date()
209
+ });
210
+
211
+ // Generate oracle seed (in production, fetch from Random.org)
212
+ const oracleSeed = crypto.randomBytes(32).toString('hex');
213
+
214
+ const { data } = await axios.post(`${API_BASE}/jackpot/oracle/reveal`, {
215
+ roundId: roundId.toString(),
216
+ oracleSeed,
217
+ });
218
+
219
+ const duration = Date.now() - startTime;
220
+
221
+ // Update state: revealed
222
+ await this.state.updateRound(roundId, {
223
+ status: 'revealed',
224
+ revealed_at: new Date(),
225
+ reveal_signature: data.signature
226
+ });
227
+
228
+ // Log action
229
+ await this.state.logAction(roundId, 'reveal', true, null, data.signature, duration);
230
+
231
+ console.log(`āœ… Randomness revealed! Sig: ${data.signature.slice(0, 8)}...`);
232
+ console.log(`šŸ“ Oracle seed: ${oracleSeed.slice(0, 16)}...`);
233
+
234
+ // Store oracle seed for verification (memory + disk)
235
+ if (this.currentRoundData) {
236
+ this.currentRoundData.oracleSeed = oracleSeed;
237
+ } else {
238
+ // Recover from disk if memory was lost
239
+ this.currentRoundData = this._loadVrfData() || {};
240
+ this.currentRoundData.oracleSeed = oracleSeed;
241
+ }
242
+ this._saveVrfData();
243
+
244
+ // VRF is written INSTANTLY when transaction confirms - no need to poll!
245
+ console.log('āœ… VRF result written on-chain! Ready to resolve immediately!');
246
+
247
+ return true;
248
+ } catch (error) {
249
+ // Error 6014 = RandomnessAlreadyConsumed - this is OK, skip to resolve
250
+ if (error.message && error.message.includes('6014')) {
251
+ console.log('ā„¹ļø Randomness already revealed, proceeding to resolve');
252
+ return true;
253
+ }
254
+ console.error('āŒ Reveal failed:', error.message);
255
+
256
+ // Log failure
257
+ await this.state.incrementRetry(roundId, error.message);
258
+ await this.state.logAction(roundId, 'reveal', false, error.message, null, Date.now() - startTime);
259
+
260
+ return false;
261
+ }
262
+ }
263
+
264
+ async waitForVrfResult() {
265
+ const [roundPda] = PublicKey.findProgramAddressSync(
266
+ [Buffer.from('round'), Buffer.alloc(8, 1)],
267
+ this.programId
268
+ );
269
+
270
+ // Poll for up to 20 seconds
271
+ for (let i = 0; i < 40; i++) {
272
+ try {
273
+ const accountInfo = await this.connection.getAccountInfo(roundPda);
274
+ if (accountInfo) {
275
+ // Check if VRF result exists (Option<u128> discriminant at offset 51)
276
+ const hasVrf = accountInfo.data[51] === 1;
277
+ if (hasVrf) {
278
+ return true;
279
+ }
280
+ }
281
+ } catch (e) {
282
+ // Ignore errors, keep polling
283
+ }
284
+ await new Promise(resolve => setTimeout(resolve, 500)); // Check every 0.5s
285
+ }
286
+ return false;
287
+ }
288
+
289
+ async resolveRound(roundId) {
290
+ console.log(`šŸ’° Resolving round ${roundId}...`);
291
+ const startTime = Date.now();
292
+
293
+ try {
294
+ // Update state: resolving
295
+ await this.state.updateRound(roundId, {
296
+ status: 'resolving',
297
+ last_attempt_at: new Date()
298
+ });
299
+
300
+ // ALWAYS use Round 1 PDAs (account reuse!)
301
+ const [roundPda] = PublicKey.findProgramAddressSync([Buffer.from('round'), Buffer.from([1,0,0,0,0,0,0,0])], this.programId);
302
+ const [entriesPda] = PublicKey.findProgramAddressSync([Buffer.from('entries'), Buffer.from([1,0,0,0,0,0,0,0])], this.programId);
303
+
304
+ const roundAccountBefore = await this.connection.getAccountInfo(roundPda);
305
+ const totalPot = roundAccountBefore.data.readBigUInt64LE(33);
306
+
307
+ // Get entries
308
+ const entriesAccount = await this.connection.getAccountInfo(entriesPda);
309
+ const entryCount = entriesAccount.data.readUInt32LE(16);
310
+
311
+ const { data } = await axios.post(`${API_BASE}/jackpot/build/resolve`, {
312
+ keeperAddress: this.wallet.publicKey.toString(),
313
+ roundId,
314
+ });
315
+
316
+ const tx = Transaction.from(Buffer.from(data.transaction, 'base64'));
317
+ tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash;
318
+ tx.feePayer = this.wallet.publicKey;
319
+ tx.sign(this.wallet);
320
+
321
+ const signature = await this.connection.sendRawTransaction(tx.serialize(), {
322
+ skipPreflight: true,
323
+ });
324
+ await this.confirmTx(signature);
325
+
326
+ const winner = data.winner;
327
+ const winAmount = Number(totalPot) * 0.95; // 95% after 5% fee
328
+
329
+ const duration = Date.now() - startTime;
330
+
331
+ // Update state: resolved
332
+ await this.state.updateRound(roundId, {
333
+ status: 'resolved',
334
+ resolved_at: new Date(),
335
+ resolve_signature: signature,
336
+ winner_pubkey: winner,
337
+ win_amount: winAmount.toString(),
338
+ total_pot: totalPot.toString(),
339
+ entry_count: entryCount
340
+ });
341
+
342
+ // Log action
343
+ await this.state.logAction(roundId, 'resolve', true, null, signature, duration);
344
+
345
+ console.log(`šŸ† Round ${roundId} resolved!`);
346
+ console.log(` Winner: ${winner.slice(0, 8)}...`);
347
+ console.log(` Prize: ${(winAmount / 1e9).toFixed(4)} SOL`);
348
+ console.log(` Sig: ${signature.slice(0, 8)}...`);
349
+
350
+ // Recover VRF data from disk if memory was lost (e.g. keeper restarted)
351
+ if (!this.currentRoundData) {
352
+ this.currentRoundData = this._loadVrfData();
353
+ if (this.currentRoundData) {
354
+ console.log('šŸ“‚ Recovered VRF data from disk');
355
+ }
356
+ }
357
+
358
+ // Capture VRF data before clearing
359
+ const vrfServerSeed = this.currentRoundData?.serverSeed || null;
360
+ const vrfServerSeedHash = this.currentRoundData?.serverSeedHash || null;
361
+ const vrfOracleSeed = this.currentRoundData?.oracleSeed || null;
362
+
363
+ // Clear current round data (memory + disk)
364
+ this.currentRoundData = null;
365
+ this._clearVrfData();
366
+
367
+ // Increment success counter
368
+ this.roundsCompleted++;
369
+ this.consecutiveFailures = 0;
370
+
371
+ // Trigger WebSocket broadcast + save history via webhook
372
+ // The SERVER process has DATABASE_URL, so its addRound writes to PostgreSQL
373
+ axios.post(`${API_BASE}/api/keeper-webhook/winner-selected`, {
374
+ roundId,
375
+ winner,
376
+ winAmount: winAmount.toString(),
377
+ totalPot: totalPot.toString(),
378
+ entryCount,
379
+ signature,
380
+ // Provably fair verification data
381
+ serverSeed: vrfServerSeed,
382
+ serverSeedHash: vrfServerSeedHash,
383
+ oracleSeed: vrfOracleSeed,
384
+ }).then(() => {
385
+ console.log(`⚔ Webhook called: winner_selected (${winner.slice(0,8)}...)`);
386
+ }).catch(err => console.warn('Webhook broadcast failed:', err.message));
387
+
388
+ return true;
389
+ } catch (error) {
390
+ console.error('āŒ Resolve failed:', error.message);
391
+
392
+ // Log failure
393
+ await this.state.incrementRetry(roundId, error.message);
394
+ await this.state.logAction(roundId, 'resolve', false, error.message, null, Date.now() - startTime);
395
+
396
+ this.consecutiveFailures++;
397
+
398
+ return false;
399
+ }
400
+ }
401
+
402
+ async openOrResetRound(previousRoundId = null) {
403
+ console.log(`šŸ“‚ Opening/resetting round...`);
404
+ const startTime = Date.now();
405
+
406
+ try {
407
+ // Check if round 1 accounts exist - if so, use reset (account reuse like Solpot!)
408
+ const roundIdBuf = Buffer.alloc(8);
409
+ roundIdBuf.writeBigUInt64LE(1n);
410
+ const [round1Pda] = PublicKey.findProgramAddressSync([Buffer.from('round'), roundIdBuf], this.programId);
411
+
412
+ const round1Exists = await this.connection.getAccountInfo(round1Pda);
413
+
414
+ if (round1Exists) {
415
+ // RESET (reuse accounts - FREE!)
416
+ console.log(`ā™»ļø Resetting round (reusing accounts - Solpot style)...`);
417
+
418
+ // Update state: resetting (if we have previous round ID)
419
+ if (previousRoundId) {
420
+ await this.state.updateRound(previousRoundId, {
421
+ status: 'resetting',
422
+ last_attempt_at: new Date()
423
+ });
424
+ }
425
+
426
+ const { data } = await axios.post(`${API_BASE}/jackpot/build/reset-round`, {
427
+ keeperAddress: this.wallet.publicKey.toString(),
428
+ });
429
+
430
+ const tx = Transaction.from(Buffer.from(data.transaction, 'base64'));
431
+ tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash;
432
+ tx.feePayer = this.wallet.publicKey;
433
+ tx.sign(this.wallet);
434
+
435
+ const signature = await this.connection.sendRawTransaction(tx.serialize(), {
436
+ skipPreflight: true,
437
+ });
438
+ await this.confirmTx(signature);
439
+
440
+ const duration = Date.now() - startTime;
441
+ const newRoundId = data.roundId;
442
+
443
+ // Update previous round: reset complete
444
+ if (previousRoundId) {
445
+ await this.state.updateRound(previousRoundId, {
446
+ reset_at: new Date(),
447
+ reset_signature: signature
448
+ });
449
+ await this.state.logAction(previousRoundId, 'reset', true, null, signature, duration);
450
+ }
451
+
452
+ // Create new round in DB
453
+ await this.state.createRound(newRoundId, 'open');
454
+
455
+ // Trigger WebSocket broadcast via webhook
456
+ axios.post(`${API_BASE}/api/keeper-webhook/round-opened`, {
457
+ roundId: newRoundId
458
+ }).catch(err => console.warn('Webhook broadcast failed:', err.message));
459
+
460
+ console.log(`ā™»ļø Round ${newRoundId} opened! (Cost: ~$0.01 gas only!) Sig: ${signature.slice(0, 8)}...`);
461
+
462
+ // Wait for RPC to sync (prevents stale data loop)
463
+ await new Promise(resolve => setTimeout(resolve, 2000));
464
+
465
+ return true;
466
+
467
+ } else {
468
+ // OPEN (first time only - pay rent)
469
+ console.log(`šŸ†• Opening FIRST round (one-time rent cost)...`);
470
+ const { data } = await axios.post(`${API_BASE}/jackpot/build/open-round`, {
471
+ keeperAddress: this.wallet.publicKey.toString(),
472
+ });
473
+
474
+ const tx = Transaction.from(Buffer.from(data.transaction, 'base64'));
475
+ tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash;
476
+ tx.feePayer = this.wallet.publicKey;
477
+ tx.sign(this.wallet);
478
+
479
+ const signature = await this.connection.sendRawTransaction(tx.serialize(), {
480
+ skipPreflight: true,
481
+ });
482
+ await this.confirmTx(signature);
483
+
484
+ const duration = Date.now() - startTime;
485
+ const newRoundId = data.roundId;
486
+
487
+ // Create new round in DB
488
+ await this.state.createRound(newRoundId, 'open');
489
+ await this.state.logAction(newRoundId, 'open', true, null, signature, duration);
490
+
491
+ console.log(`šŸŽ‰ Round ${newRoundId} opened! (Paid rent once) Sig: ${signature.slice(0, 8)}...`);
492
+ return true;
493
+ }
494
+ } catch (error) {
495
+ console.error('āŒ Open/reset failed:', error.message || error);
496
+ if (error.response) {
497
+ console.error('API Error:', error.response.data);
498
+ }
499
+ if (error.logs) {
500
+ console.error('Transaction logs:', error.logs);
501
+ }
502
+
503
+ // Log failure
504
+ if (previousRoundId) {
505
+ await this.state.logAction(previousRoundId, 'reset', false, error.message, null, Date.now() - startTime);
506
+ }
507
+
508
+ return false;
509
+ }
510
+ }
511
+
512
+ async processRound(round) {
513
+ const roundId = round.roundId;
514
+ const entryCount = round.entryCount || 0;
515
+
516
+ // Ensure round exists in database
517
+ let dbRound = await this.state.getRound(roundId);
518
+ if (!dbRound) {
519
+ await this.state.createRound(roundId, round.status.toLowerCase());
520
+ }
521
+
522
+ if (round.status === 'Open') {
523
+ // Check if time expired
524
+ if (round.timeRemainingSlots <= 0) {
525
+ console.log(`\nā° Round ${roundId} timer expired! (${entryCount} entries)`);
526
+
527
+ // Skip if no entries - just reset/open new round
528
+ if (entryCount === 0) {
529
+ console.log(`āš ļø Round ${roundId} has no entries, resetting...`);
530
+ await this.openOrResetRound(roundId);
531
+ return;
532
+ }
533
+
534
+ // Lock → Reveal Randomness → Resolve → Open (3-step VRF flow)
535
+ if (await this.lockRound(roundId)) {
536
+ if (await this.revealRandomness(roundId)) {
537
+ if (await this.resolveRound(roundId)) {
538
+ // Wait 5s for carousel animation + user to see winner
539
+ console.log('ā³ Waiting 5s for winner animation...');
540
+ await new Promise(resolve => setTimeout(resolve, 5000));
541
+ await this.openOrResetRound(roundId);
542
+ }
543
+ }
544
+ }
545
+ } else {
546
+ const secsLeft = Math.floor(round.timeRemainingSlots * 0.4);
547
+ console.log(`ā³ Round ${roundId} - ${secsLeft}s left, ${entryCount} entries, pot: ${(Number(round.totalPotLamports)/1e9).toFixed(3)} SOL`);
548
+
549
+ // CRITICAL: Broadcast timer update to all clients every check
550
+ axios.post(`${API_BASE}/api/keeper-webhook/timer-update`, {
551
+ roundId,
552
+ timeRemaining: secsLeft,
553
+ entryCount,
554
+ totalPot: round.totalPotLamports,
555
+ status: round.status
556
+ }).catch(err => console.warn('Timer broadcast failed:', err.message));
557
+
558
+ // Also track entry count changes for logging
559
+ if (this.lastEntryCount[roundId] !== entryCount) {
560
+ this.lastEntryCount[roundId] = entryCount;
561
+ }
562
+ }
563
+ } else if (round.status === 'Locked') {
564
+ // Check if we already resolved this round (prevent loops)
565
+ if (this.lastResolvedRound === roundId) {
566
+ console.log(`āš ļø Already resolved round ${roundId}, forcing reset...`);
567
+ await this.openOrResetRound();
568
+ return;
569
+ }
570
+
571
+ console.log(`šŸ”’ Round ${roundId} locked, paying out...`);
572
+
573
+ // Track consecutive failures for this locked round
574
+ this.lockedRoundFailures = this.lockedRoundFailures || {};
575
+ this.lockedRoundFailures[roundId] = this.lockedRoundFailures[roundId] || 0;
576
+
577
+ // Try to resolve once
578
+ if (await this.resolveRound(roundId)) {
579
+ this.lastResolvedRound = roundId; // Track it
580
+ this.lockedRoundFailures[roundId] = 0; // Reset counter
581
+ await new Promise(resolve => setTimeout(resolve, 5000)); // Same 5s delay
582
+ await this.openOrResetRound();
583
+ } else {
584
+ this.lockedRoundFailures[roundId]++;
585
+ console.log(`āŒ Resolve failed (attempt ${this.lockedRoundFailures[roundId]}), will retry next cycle`);
586
+
587
+ // After 5 consecutive failures, try to force reset (round is stuck without VRF)
588
+ if (this.lockedRoundFailures[roundId] >= 5) {
589
+ console.log(`🚨 Round ${roundId} stuck after 5 attempts! VRF may not have been consumed.`);
590
+ console.log(`ā™»ļø Forcing reset to start fresh round...`);
591
+ this.lockedRoundFailures[roundId] = 0;
592
+ await this.openOrResetRound(roundId);
593
+ }
594
+ }
595
+ } else if (round.status === 'Resolved') {
596
+ console.log(`āœ… Round ${roundId} resolved, resetting...`);
597
+ await this.openOrResetRound();
598
+ }
599
+ }
600
+
601
+ async recoverStuckRounds() {
602
+ console.log('šŸ”§ Checking for stuck rounds...');
603
+
604
+ try {
605
+ const stuckRounds = await this.state.getStuckRounds();
606
+
607
+ if (stuckRounds.length === 0) {
608
+ console.log('āœ… No stuck rounds found');
609
+ return;
610
+ }
611
+
612
+ console.log(`āš ļø Found ${stuckRounds.length} stuck round(s)!`);
613
+
614
+ for (const stuck of stuckRounds) {
615
+ console.log(`\nšŸ”§ Recovering round ${stuck.round_id} stuck in '${stuck.status}' for ${Math.floor(stuck.seconds_stuck)}s`);
616
+ console.log(` Retries so far: ${stuck.retry_count}`);
617
+ console.log(` Last error: ${stuck.last_error || 'none'}`);
618
+
619
+ // Get current on-chain state
620
+ const round = await this.getCurrentRound();
621
+
622
+ if (!round || round.roundId !== stuck.round_id) {
623
+ console.log(` Round ${stuck.round_id} no longer active on-chain, marking resolved in DB`);
624
+ await this.state.updateRound(stuck.round_id, { status: 'resolved', resolved_at: new Date() });
625
+ continue;
626
+ }
627
+
628
+ // Resume based on current state
629
+ console.log(` On-chain status: ${round.status}, DB status: ${stuck.status}`);
630
+
631
+ if (stuck.status === 'locking' || stuck.status === 'locked') {
632
+ if (round.status === 'Locked') {
633
+ console.log(` āœ“ Lock completed, moving to reveal`);
634
+ await this.state.updateRound(stuck.round_id, { status: 'locked' });
635
+ await this.revealRandomness(round.roundId);
636
+ } else {
637
+ console.log(` Retrying lock...`);
638
+ await this.lockRound(round.roundId);
639
+ }
640
+ }
641
+
642
+ else if (stuck.status === 'revealing' || stuck.status === 'revealed') {
643
+ if (round.status === 'Locked') {
644
+ console.log(` Retrying reveal...`);
645
+ await this.revealRandomness(round.roundId);
646
+ } else {
647
+ console.log(` Status mismatch, skipping to resolve`);
648
+ }
649
+ }
650
+
651
+ else if (stuck.status === 'resolving') {
652
+ console.log(` Retrying resolve...`);
653
+ await this.resolveRound(round.roundId);
654
+ }
655
+
656
+ else if (stuck.status === 'resetting') {
657
+ console.log(` Retrying reset...`);
658
+ await this.openOrResetRound(stuck.round_id);
659
+ }
660
+ }
661
+
662
+ console.log('\nāœ… Recovery complete\n');
663
+
664
+ } catch (error) {
665
+ console.error('āŒ Recovery failed:', error.message);
666
+ }
667
+ }
668
+
669
+ async recordHealthMetrics() {
670
+ const uptime = Math.floor((Date.now() - this.startTime) / 1000);
671
+
672
+ await this.state.recordHealth({
673
+ roundsCompleted: this.roundsCompleted,
674
+ consecutiveFailures: this.consecutiveFailures,
675
+ lastSuccessRound: this.lastResolvedRound || 0,
676
+ lastError: this.lastError || null,
677
+ uptimeSeconds: uptime
678
+ });
679
+ }
680
+
681
+ async run() {
682
+ console.log('šŸš€ Keeper bot started!\n');
683
+
684
+ // Run recovery on startup
685
+ await this.recoverStuckRounds();
686
+
687
+ // Check if round exists, if not open/reset one
688
+ let round = await this.getCurrentRound();
689
+ if (!round) {
690
+ console.log('No active round, opening/resetting...');
691
+ await this.openOrResetRound();
692
+ }
693
+
694
+ let healthCheckCounter = 0;
695
+ let lastHealthTime = Date.now();
696
+ let lastRecoveryTime = Date.now();
697
+
698
+ // Main loop
699
+ while (true) {
700
+ try {
701
+ round = await this.getCurrentRound();
702
+
703
+ if (round) {
704
+ await this.processRound(round);
705
+ } else {
706
+ console.log('No round found, opening/resetting...');
707
+ await this.openOrResetRound();
708
+ }
709
+
710
+ // Health check every ~60 seconds
711
+ if (Date.now() - lastHealthTime >= 60000) {
712
+ await this.recordHealthMetrics();
713
+ lastHealthTime = Date.now();
714
+ }
715
+
716
+ // Check for stuck rounds every ~5 minutes
717
+ if (Date.now() - lastRecoveryTime >= 300000) {
718
+ await this.recoverStuckRounds();
719
+ lastRecoveryTime = Date.now();
720
+ }
721
+
722
+ // Adaptive poll interval based on time remaining
723
+ const secsLeft = round ? Math.floor(round.timeRemainingSlots * 0.4) : 0;
724
+ const pollMs = secsLeft > 86400 ? 300000 // > 1 day → 5 minutes
725
+ : secsLeft > 3600 ? 60000 // > 1 hour → 60 seconds
726
+ : secsLeft > 300 ? 5000 // > 5 min → 5 seconds
727
+ : 2000; // ≤ 5 min → 2 seconds (action imminent)
728
+
729
+ await new Promise(resolve => setTimeout(resolve, pollMs));
730
+
731
+ } catch (error) {
732
+ console.error('āŒ Error:', error.message);
733
+ this.lastError = error.message;
734
+ await new Promise(resolve => setTimeout(resolve, 10000));
735
+ }
736
+ }
737
+ }
738
+ }
739
+
740
+ const keeper = new JackpotKeeper();
741
+ keeper.run().catch(console.error);
742
+