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,856 @@
1
+ /**
2
+ * 🎰 Jackpot Service
3
+ *
4
+ * Handles Solpot-style continuous jackpot rounds with provably fair randomness
5
+ * Features:
6
+ * - Continuous round cycles
7
+ * - Weighted entry odds (bigger bets = higher chance)
8
+ * - Commit-reveal randomness (like Solpot.com)
9
+ * - Permissionless keeper operations
10
+ * - Protocol fee collection
11
+ */
12
+
13
+ const { Connection, Keypair, PublicKey, SystemProgram, Transaction, TransactionInstruction } = require('@solana/web3.js');
14
+ const crypto = require('crypto');
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+
18
+ // 🎰 Jackpot Program Configuration (configurable via env vars for multi-environment)
19
+ const JACKPOT_PROGRAM_ID = new PublicKey(
20
+ process.env.JACKPOT_PROGRAM_ID || 'BHidyz25KWkNPdTHgeANzMg25MM2KEiNnG4yE5F46XUz'
21
+ );
22
+
23
+ // 🔐 ORACLE WALLET - Submits random seeds (like Solpot's Random.org)
24
+ const ORACLE_WALLET = new PublicKey(
25
+ process.env.JACKPOT_ORACLE_WALLET || 'FWUJCthDfPcgmTvdQWM5uofxxiYjqJFMMwiLYvS7LBFa'
26
+ );
27
+
28
+ // Instruction discriminators (SHA256 hash of "global:instruction_name")
29
+ const DISCRIMINATORS = {
30
+ INITIALIZE: Buffer.from([175, 175, 109, 31, 13, 152, 155, 237]), // initialize
31
+ OPEN_ROUND: Buffer.from([66, 235, 123, 240, 8, 35, 185, 159]), // open_round
32
+ ENTER_ROUND: Buffer.from([166, 162, 71, 230, 92, 51, 37, 43]), // enter_round
33
+ LOCK_ROUND: Buffer.from([68, 124, 43, 230, 30, 44, 248, 227]), // lock_round
34
+ CONSUME_RANDOMNESS: Buffer.from([190, 217, 49, 162, 99, 26, 73, 234]), // consume_randomness
35
+ RESOLVE_ROUND: Buffer.from([165, 114, 237, 158, 1, 36, 70, 254]), // resolve_round
36
+ UPDATE_CONFIG: Buffer.from([29, 158, 252, 191, 10, 83, 219, 99]), // update_config
37
+ RESET_ROUND: Buffer.from([199, 184, 226, 203, 15, 56, 59, 113]), // reset_round (account reuse!)
38
+ };
39
+
40
+ class JackpotService {
41
+ constructor(config) {
42
+ this.connection = new Connection(config.rpcUrl || 'http://127.0.0.1:8899', 'confirmed');
43
+ this.programId = JACKPOT_PROGRAM_ID;
44
+ this.oracleWallet = ORACLE_WALLET;
45
+ this.walletsDir = config.walletsDir || path.join(__dirname, '..', 'wallets');
46
+
47
+ // Load oracle keypair - Priority: ENV var > wallet file
48
+ try {
49
+ let secretKey;
50
+
51
+ if (process.env.KEEPER_PRIVATE_KEY) {
52
+ // For Heroku - use environment variable
53
+ console.log('🔐 Oracle keypair loaded from KEEPER_PRIVATE_KEY env var');
54
+ secretKey = JSON.parse(process.env.KEEPER_PRIVATE_KEY);
55
+ this.oracleKeypair = Keypair.fromSecretKey(Uint8Array.from(secretKey));
56
+ } else {
57
+ // For local - use wallet file
58
+ const oracleKeyPath = path.join(this.walletsDir, 'jackpot_oracle.json');
59
+ if (fs.existsSync(oracleKeyPath)) {
60
+ secretKey = JSON.parse(fs.readFileSync(oracleKeyPath, 'utf-8'));
61
+ this.oracleKeypair = Keypair.fromSecretKey(Uint8Array.from(secretKey));
62
+ console.log('🔐 Oracle keypair loaded from wallet file');
63
+ }
64
+ }
65
+ } catch (e) {
66
+ console.log('⚠️ Oracle keypair not found - oracle functions will be limited');
67
+ }
68
+
69
+ console.log('🎰 Jackpot Service initialized');
70
+
71
+ // Polling-based tx confirmation (Alchemy doesn't support signatureSubscribe websocket)
72
+ this.confirmTx = async (signature, timeout = 60000) => {
73
+ const start = Date.now();
74
+ while (Date.now() - start < timeout) {
75
+ const statuses = await this.connection.getSignatureStatuses([signature]);
76
+ const status = statuses?.value?.[0];
77
+ if (status?.err) throw new Error(`Transaction failed: ${JSON.stringify(status.err)}`);
78
+ if (status?.confirmationStatus === 'confirmed' || status?.confirmationStatus === 'finalized') return;
79
+ await new Promise(resolve => setTimeout(resolve, 2000));
80
+ }
81
+ throw new Error('Transaction confirmation timeout');
82
+ };
83
+ console.log(' Program ID:', this.programId.toString());
84
+ console.log(' Oracle:', this.oracleWallet.toString());
85
+ }
86
+
87
+ /**
88
+ * Get Config PDA
89
+ */
90
+ getConfigPDA() {
91
+ return PublicKey.findProgramAddressSync(
92
+ [Buffer.from('config')],
93
+ this.programId
94
+ );
95
+ }
96
+
97
+ /**
98
+ * Get Round PDA from round ID
99
+ */
100
+ getRoundPDA(roundId) {
101
+ const roundIdBuf = Buffer.alloc(8);
102
+ roundIdBuf.writeBigUInt64LE(BigInt(roundId));
103
+ return PublicKey.findProgramAddressSync(
104
+ [Buffer.from('round'), roundIdBuf],
105
+ this.programId
106
+ );
107
+ }
108
+
109
+ /**
110
+ * Get Entries PDA from round ID
111
+ */
112
+ getEntriesPDA(roundId) {
113
+ const roundIdBuf = Buffer.alloc(8);
114
+ roundIdBuf.writeBigUInt64LE(BigInt(roundId));
115
+ return PublicKey.findProgramAddressSync(
116
+ [Buffer.from('entries'), roundIdBuf],
117
+ this.programId
118
+ );
119
+ }
120
+
121
+ /**
122
+ * Get protocol config
123
+ */
124
+ async getConfig() {
125
+ const [configPda] = this.getConfigPDA();
126
+
127
+ try {
128
+ const accountInfo = await this.connection.getAccountInfo(configPda);
129
+ if (!accountInfo) {
130
+ return null;
131
+ }
132
+
133
+ // Parse config data (simplified - you'd use Borsh in production)
134
+ const data = accountInfo.data;
135
+
136
+ const roundDurationSlots = data.readBigUInt64LE(74);
137
+ // ~0.4 seconds per slot on Solana
138
+ const roundDurationSeconds = Math.floor(Number(roundDurationSlots) * 0.4);
139
+
140
+ return {
141
+ address: configPda.toString(),
142
+ authority: new PublicKey(data.slice(8, 40)).toString(),
143
+ treasury: new PublicKey(data.slice(40, 72)).toString(),
144
+ feeBasisPoints: data.readUInt16LE(72),
145
+ roundDurationSlots: roundDurationSlots.toString(),
146
+ roundDurationSeconds,
147
+ currentRoundId: data.readBigUInt64LE(82).toString(),
148
+ totalVolume: data.readBigUInt64LE(90).toString(),
149
+ totalFeesCollected: data.readBigUInt64LE(98).toString(),
150
+ initialized: true,
151
+ };
152
+ } catch (error) {
153
+ console.error('Error fetching config:', error);
154
+ return null;
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Get current round info
160
+ * NOTE: With account reuse, we always use Round 1 PDAs!
161
+ * FORCES FRESH READ - NO CACHING
162
+ */
163
+ async getCurrentRound() {
164
+ // Force fresh config read
165
+ const [configPda] = this.getConfigPDA();
166
+ const configAccount = await this.connection.getAccountInfo(configPda);
167
+ if (!configAccount) {
168
+ return null;
169
+ }
170
+
171
+ const currentRoundId = configAccount.data.readBigUInt64LE(82);
172
+ if (currentRoundId === 0n) {
173
+ return null;
174
+ }
175
+
176
+ // Force fresh round + entries read from Round 1 PDAs
177
+ const [roundPda] = this.getRoundPDA(1);
178
+ const [entriesPda] = this.getEntriesPDA(1);
179
+ const [roundAccount, entriesAccount] = await Promise.all([
180
+ this.connection.getAccountInfo(roundPda),
181
+ this.connection.getAccountInfo(entriesPda),
182
+ ]);
183
+ if (!roundAccount) {
184
+ return null;
185
+ }
186
+
187
+ // Parse round data directly (NO CACHE)
188
+ const actualRoundId = roundAccount.data.readBigUInt64LE(8);
189
+ const status = roundAccount.data[16];
190
+ const statusNames = ['Open', 'Locked', 'Resolved'];
191
+ const currentSlot = await this.connection.getSlot();
192
+
193
+ // Read entryCount from Entries PDA (offset 16, always correct)
194
+ // The Round PDA entryCount offset is unreliable.
195
+ const entryCount = entriesAccount ? entriesAccount.data.readUInt32LE(16) : 0;
196
+
197
+ return {
198
+ address: roundPda.toString(),
199
+ roundId: actualRoundId.toString(),
200
+ status: statusNames[status] || 'Unknown',
201
+ startSlot: roundAccount.data.readBigUInt64LE(17).toString(),
202
+ endSlot: roundAccount.data.readBigUInt64LE(25).toString(),
203
+ totalPotLamports: roundAccount.data.readBigUInt64LE(33).toString(),
204
+ totalWeight: roundAccount.data.readBigUInt64LE(41).toString(),
205
+ entryCount,
206
+ currentSlot: currentSlot.toString(),
207
+ timeRemainingSlots: Math.max(0, Number(roundAccount.data.readBigUInt64LE(25)) - currentSlot),
208
+ };
209
+ }
210
+
211
+ /**
212
+ * Get specific round info
213
+ * NOTE: When account reuse is active, always pass roundId=1
214
+ */
215
+ async getRoundInfo(roundId) {
216
+ const [roundPda] = this.getRoundPDA(roundId);
217
+ const [entriesPda] = this.getEntriesPDA(roundId);
218
+
219
+ try {
220
+ const [accountInfo, entriesInfo] = await Promise.all([
221
+ this.connection.getAccountInfo(roundPda),
222
+ this.connection.getAccountInfo(entriesPda),
223
+ ]);
224
+ if (!accountInfo) {
225
+ return null;
226
+ }
227
+
228
+ const data = accountInfo.data;
229
+
230
+ // Parse round data (read actual round_id from account)
231
+ const actualRoundId = data.readBigUInt64LE(8); // First field after discriminator
232
+ const status = data[16]; // 0=Open, 1=Locked, 2=Resolved
233
+ const statusNames = ['Open', 'Locked', 'Resolved'];
234
+
235
+ const currentSlot = await this.connection.getSlot();
236
+
237
+ // Read entryCount from Entries PDA (offset 16, always correct)
238
+ const entryCount = entriesInfo ? entriesInfo.data.readUInt32LE(16) : 0;
239
+
240
+ return {
241
+ address: roundPda.toString(),
242
+ roundId: actualRoundId.toString(), // Use actual round_id from account!
243
+ status: statusNames[status] || 'Unknown',
244
+ startSlot: data.readBigUInt64LE(17).toString(),
245
+ endSlot: data.readBigUInt64LE(25).toString(),
246
+ totalPotLamports: data.readBigUInt64LE(33).toString(),
247
+ totalWeight: data.readBigUInt64LE(41).toString(),
248
+ entryCount,
249
+ currentSlot: currentSlot.toString(),
250
+ timeRemainingSlots: Math.max(0, Number(data.readBigUInt64LE(25)) - currentSlot),
251
+ };
252
+ } catch (error) {
253
+ console.error('Error fetching round:', error);
254
+ return null;
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Get entries for a round
260
+ * NOTE: Always uses Round 1 PDA due to account reuse!
261
+ */
262
+ async getRoundEntries(roundId) {
263
+ const [entriesPda] = this.getEntriesPDA(1); // Always Round 1!
264
+
265
+ try {
266
+ const accountInfo = await this.connection.getAccountInfo(entriesPda);
267
+ if (!accountInfo) {
268
+ console.log('⚠️ Entries account not found');
269
+ return [];
270
+ }
271
+
272
+ // Parse entries - Anchor layout:
273
+ // 8 bytes: discriminator
274
+ // 8 bytes: round_id (u64)
275
+ // 4 bytes: vec length (u32)
276
+ // entries: player(32) + weight(8) + cumulative_to(16) = 56 bytes each
277
+ const data = accountInfo.data;
278
+
279
+ if (data.length < 20) {
280
+ console.log('⚠️ Entries account too small:', data.length);
281
+ return [];
282
+ }
283
+
284
+ const entryCount = data.readUInt32LE(16); // After discriminator(8) + round_id(8)
285
+ console.log(`📊 Entries account: ${data.length} bytes, ${entryCount} entries claimed`);
286
+
287
+ const entries = [];
288
+ let offset = 20; // Start after: discriminator(8) + round_id(8) + vec_len(4)
289
+
290
+ for (let i = 0; i < entryCount; i++) {
291
+ if (offset + 56 > data.length) {
292
+ console.log(`⚠️ Not enough data for entry ${i} at offset ${offset}`);
293
+ break;
294
+ }
295
+
296
+ const player = new PublicKey(data.slice(offset, offset + 32));
297
+ const weight = data.readBigUInt64LE(offset + 32);
298
+ // Read u128 cumulative_to (16 bytes) - use lower 64 bits
299
+ const cumulativeToLow = data.readBigUInt64LE(offset + 40);
300
+
301
+ entries.push({
302
+ player: player.toString(),
303
+ weight: weight.toString(),
304
+ cumulativeTo: cumulativeToLow.toString(),
305
+ });
306
+
307
+ offset += 56; // 32 (pubkey) + 8 (weight) + 16 (cumulative_to u128)
308
+ }
309
+
310
+ console.log(`✅ Successfully parsed ${entries.length} entries`);
311
+ return entries;
312
+ } catch (error) {
313
+ console.error('❌ Error fetching entries:', error);
314
+ return [];
315
+ }
316
+ }
317
+
318
+ /**
319
+ * BUILD TRANSACTION: Initialize protocol (admin only)
320
+ */
321
+ async buildInitializeTransaction({ adminAddress, treasuryAddress, feeBasisPoints, roundDurationSlots }) {
322
+ const [configPda] = this.getConfigPDA();
323
+ const admin = new PublicKey(adminAddress);
324
+ const treasury = new PublicKey(treasuryAddress);
325
+
326
+ const data = Buffer.concat([
327
+ DISCRIMINATORS.INITIALIZE,
328
+ this.serializeU16(feeBasisPoints),
329
+ this.serializeU64(roundDurationSlots),
330
+ ]);
331
+
332
+ const keys = [
333
+ { pubkey: configPda, isSigner: false, isWritable: true },
334
+ { pubkey: admin, isSigner: true, isWritable: true },
335
+ { pubkey: treasury, isSigner: false, isWritable: false },
336
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
337
+ ];
338
+
339
+ const instruction = new TransactionInstruction({
340
+ keys,
341
+ programId: this.programId,
342
+ data,
343
+ });
344
+
345
+ const transaction = new Transaction().add(instruction);
346
+ transaction.feePayer = admin;
347
+ transaction.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash;
348
+
349
+ return {
350
+ transaction: transaction.serialize({ requireAllSignatures: false }).toString('base64'),
351
+ configPda: configPda.toString(),
352
+ };
353
+ }
354
+
355
+ /**
356
+ * BUILD TRANSACTION: Update protocol config (authority only)
357
+ * Uses Anchor Option<T> encoding: 0 byte = None, 1 byte + value = Some
358
+ */
359
+ async buildUpdateConfigTransaction({ authorityAddress, feeBasisPoints, roundDurationSlots, newAuthority }) {
360
+ const [configPda] = this.getConfigPDA();
361
+ const authority = new PublicKey(authorityAddress);
362
+
363
+ // Serialize Anchor Option<u16> for fee_basis_points
364
+ const feeOption = feeBasisPoints != null
365
+ ? Buffer.concat([Buffer.from([1]), this.serializeU16(feeBasisPoints)])
366
+ : Buffer.from([0]);
367
+
368
+ // Serialize Anchor Option<u64> for round_duration_slots
369
+ const durationOption = roundDurationSlots != null
370
+ ? Buffer.concat([Buffer.from([1]), this.serializeU64(roundDurationSlots)])
371
+ : Buffer.from([0]);
372
+
373
+ // Serialize Anchor Option<Pubkey> for new_authority
374
+ const authorityOption = newAuthority != null
375
+ ? Buffer.concat([Buffer.from([1]), new PublicKey(newAuthority).toBuffer()])
376
+ : Buffer.from([0]);
377
+
378
+ const data = Buffer.concat([
379
+ DISCRIMINATORS.UPDATE_CONFIG,
380
+ feeOption,
381
+ durationOption,
382
+ authorityOption,
383
+ ]);
384
+
385
+ const keys = [
386
+ { pubkey: configPda, isSigner: false, isWritable: true },
387
+ { pubkey: authority, isSigner: true, isWritable: false },
388
+ ];
389
+
390
+ const instruction = new TransactionInstruction({
391
+ keys,
392
+ programId: this.programId,
393
+ data,
394
+ });
395
+
396
+ const transaction = new Transaction().add(instruction);
397
+ transaction.feePayer = authority;
398
+ transaction.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash;
399
+
400
+ return {
401
+ transaction: transaction.serialize({ requireAllSignatures: false }).toString('base64'),
402
+ configPda: configPda.toString(),
403
+ };
404
+ }
405
+
406
+ /**
407
+ * BUILD TRANSACTION: Open a new round
408
+ */
409
+ async buildOpenRoundTransaction({ keeperAddress }) {
410
+ const [configPda] = this.getConfigPDA();
411
+ const config = await this.getConfig();
412
+
413
+ if (!config) {
414
+ throw new Error('Protocol not initialized');
415
+ }
416
+
417
+ const nextRoundId = BigInt(config.currentRoundId) + 1n;
418
+ const [roundPda] = this.getRoundPDA(nextRoundId);
419
+ const [entriesPda] = this.getEntriesPDA(nextRoundId);
420
+ const keeper = new PublicKey(keeperAddress);
421
+
422
+ const data = DISCRIMINATORS.OPEN_ROUND;
423
+
424
+ const keys = [
425
+ { pubkey: configPda, isSigner: false, isWritable: true },
426
+ { pubkey: roundPda, isSigner: false, isWritable: true },
427
+ { pubkey: entriesPda, isSigner: false, isWritable: true },
428
+ { pubkey: keeper, isSigner: true, isWritable: true },
429
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
430
+ ];
431
+
432
+ const instruction = new TransactionInstruction({
433
+ keys,
434
+ programId: this.programId,
435
+ data,
436
+ });
437
+
438
+ const transaction = new Transaction().add(instruction);
439
+ transaction.feePayer = keeper;
440
+ transaction.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash;
441
+
442
+ return {
443
+ transaction: transaction.serialize({ requireAllSignatures: false }).toString('base64'),
444
+ roundId: nextRoundId.toString(),
445
+ roundPda: roundPda.toString(),
446
+ entriesPda: entriesPda.toString(),
447
+ };
448
+ }
449
+
450
+ /**
451
+ * BUILD TRANSACTION: Enter a round
452
+ * NOTE: Always uses Round 1 PDAs due to account reuse!
453
+ */
454
+ async buildEnterRoundTransaction({ playerAddress, amount, roundId }) {
455
+ const [configPda] = this.getConfigPDA();
456
+ const config = await this.getConfig();
457
+
458
+ if (!config) {
459
+ throw new Error('Protocol not initialized');
460
+ }
461
+
462
+ // Always use Round 1 PDAs (account reuse!)
463
+ const [roundPda] = this.getRoundPDA(1);
464
+ const [entriesPda] = this.getEntriesPDA(1);
465
+ const player = new PublicKey(playerAddress);
466
+ const actualRoundId = config.currentRoundId;
467
+
468
+ const data = Buffer.concat([
469
+ DISCRIMINATORS.ENTER_ROUND,
470
+ this.serializeU64(amount),
471
+ ]);
472
+
473
+ const keys = [
474
+ { pubkey: configPda, isSigner: false, isWritable: true },
475
+ { pubkey: roundPda, isSigner: false, isWritable: true },
476
+ { pubkey: entriesPda, isSigner: false, isWritable: true },
477
+ { pubkey: player, isSigner: true, isWritable: true },
478
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
479
+ ];
480
+
481
+ const instruction = new TransactionInstruction({
482
+ keys,
483
+ programId: this.programId,
484
+ data,
485
+ });
486
+
487
+ const transaction = new Transaction().add(instruction);
488
+ transaction.feePayer = player;
489
+ transaction.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash;
490
+
491
+ return {
492
+ transaction: transaction.serialize({ requireAllSignatures: false }).toString('base64'),
493
+ roundId: actualRoundId.toString(),
494
+ amount: amount.toString(),
495
+ };
496
+ }
497
+
498
+ /**
499
+ * BUILD TRANSACTION: Enter round (sponsored - treasury pays)
500
+ * Compound tx: SystemProgram.transfer(treasury → player) + enter_round(player, amount)
501
+ * NOTE: Always uses Round 1 PDAs due to account reuse!
502
+ */
503
+ async buildEnterRoundSponsoredTransaction({ playerAddress, amount, treasuryKeypair }) {
504
+ const [configPda] = this.getConfigPDA();
505
+ const config = await this.getConfig();
506
+
507
+ if (!config) {
508
+ throw new Error('Protocol not initialized');
509
+ }
510
+
511
+ // Always use Round 1 PDAs (account reuse!)
512
+ const [roundPda] = this.getRoundPDA(1);
513
+ const [entriesPda] = this.getEntriesPDA(1);
514
+ const player = new PublicKey(playerAddress);
515
+ const actualRoundId = config.currentRoundId;
516
+
517
+ // IX 1: Treasury transfers SOL to player
518
+ const transferIx = SystemProgram.transfer({
519
+ fromPubkey: treasuryKeypair.publicKey,
520
+ toPubkey: player,
521
+ lamports: BigInt(amount),
522
+ });
523
+
524
+ // IX 2: Player enters round (identical to normal enter)
525
+ const enterData = Buffer.concat([
526
+ DISCRIMINATORS.ENTER_ROUND,
527
+ this.serializeU64(amount),
528
+ ]);
529
+
530
+ const enterIx = new TransactionInstruction({
531
+ keys: [
532
+ { pubkey: configPda, isSigner: false, isWritable: true },
533
+ { pubkey: roundPda, isSigner: false, isWritable: true },
534
+ { pubkey: entriesPda, isSigner: false, isWritable: true },
535
+ { pubkey: player, isSigner: true, isWritable: true },
536
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
537
+ ],
538
+ programId: this.programId,
539
+ data: enterData,
540
+ });
541
+
542
+ // Build compound transaction
543
+ const transaction = new Transaction().add(transferIx).add(enterIx);
544
+
545
+ // Use 'finalized' commitment for more stable blockhash (lasts longer)
546
+ const { blockhash, lastValidBlockHeight } = await this.connection.getLatestBlockhash('finalized');
547
+ transaction.recentBlockhash = blockhash;
548
+ transaction.lastValidBlockHeight = lastValidBlockHeight;
549
+ transaction.feePayer = treasuryKeypair.publicKey; // Treasury pays tx fee
550
+
551
+ // Treasury signs first (server-side)
552
+ transaction.partialSign(treasuryKeypair);
553
+
554
+ return {
555
+ transaction: transaction.serialize({ requireAllSignatures: false }).toString('base64'),
556
+ roundId: actualRoundId.toString(),
557
+ amount: amount.toString(),
558
+ sponsored: true,
559
+ };
560
+ }
561
+
562
+ /**
563
+ * BUILD TRANSACTION: Lock round (commit phase)
564
+ * NOTE: Always uses Round 1 PDA due to account reuse!
565
+ */
566
+ async buildLockRoundTransaction({ keeperAddress, roundId }) {
567
+ const [configPda] = this.getConfigPDA();
568
+ const config = await this.getConfig();
569
+
570
+ if (!config) {
571
+ throw new Error('Protocol not initialized');
572
+ }
573
+
574
+ // Always use Round 1 PDAs (account reuse!)
575
+ const [roundPda] = this.getRoundPDA(1);
576
+ const [entriesPda] = this.getEntriesPDA(1);
577
+ const keeper = new PublicKey(keeperAddress);
578
+ const actualRoundId = config.currentRoundId;
579
+
580
+ // Generate server seed hash (commit phase)
581
+ const serverSeed = crypto.randomBytes(32);
582
+ const serverSeedHash = crypto.createHash('sha256').update(serverSeed).digest();
583
+
584
+ const data = Buffer.concat([
585
+ DISCRIMINATORS.LOCK_ROUND,
586
+ serverSeedHash, // 32 bytes
587
+ ]);
588
+
589
+ // LockRound struct has only 3 accounts: config, round, keeper (NO entries!)
590
+ const keys = [
591
+ { pubkey: configPda, isSigner: false, isWritable: true },
592
+ { pubkey: roundPda, isSigner: false, isWritable: true },
593
+ { pubkey: keeper, isSigner: true, isWritable: true },
594
+ ];
595
+
596
+ const instruction = new TransactionInstruction({
597
+ keys,
598
+ programId: this.programId,
599
+ data,
600
+ });
601
+
602
+ const transaction = new Transaction().add(instruction);
603
+ transaction.feePayer = keeper;
604
+ transaction.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash;
605
+
606
+ return {
607
+ transaction: transaction.serialize({ requireAllSignatures: false }).toString('base64'),
608
+ roundId: actualRoundId.toString(),
609
+ serverSeed: serverSeed.toString('hex'),
610
+ serverSeedHash: serverSeedHash.toString('hex'),
611
+ };
612
+ }
613
+
614
+ /**
615
+ * ORACLE: Consume randomness (reveal phase)
616
+ * This is called by the oracle after lock_round
617
+ * NOTE: Always uses Round 1 PDA due to account reuse!
618
+ */
619
+ async consumeRandomness({ roundId, oracleSeed }) {
620
+ if (!this.oracleKeypair) {
621
+ throw new Error('Oracle keypair not loaded');
622
+ }
623
+
624
+ const [configPda] = this.getConfigPDA();
625
+ const [roundPda] = this.getRoundPDA(1); // Always Round 1!
626
+
627
+ // Generate oracle seed from Random.org or crypto.randomBytes
628
+ const seedBuffer = oracleSeed || crypto.randomBytes(32);
629
+
630
+ const data = Buffer.concat([
631
+ DISCRIMINATORS.CONSUME_RANDOMNESS,
632
+ seedBuffer, // 32 bytes
633
+ ]);
634
+
635
+ const keys = [
636
+ { pubkey: configPda, isSigner: false, isWritable: true },
637
+ { pubkey: roundPda, isSigner: false, isWritable: true },
638
+ { pubkey: this.oracleKeypair.publicKey, isSigner: true, isWritable: false },
639
+ ];
640
+
641
+ const instruction = new TransactionInstruction({
642
+ keys,
643
+ programId: this.programId,
644
+ data,
645
+ });
646
+
647
+ const transaction = new Transaction().add(instruction);
648
+ transaction.feePayer = this.oracleKeypair.publicKey;
649
+ transaction.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash;
650
+
651
+ // Sign and send
652
+ transaction.sign(this.oracleKeypair);
653
+ const signature = await this.connection.sendRawTransaction(transaction.serialize(), {
654
+ skipPreflight: true,
655
+ });
656
+ await this.confirmTx(signature);
657
+
658
+ return {
659
+ signature,
660
+ roundId: roundId.toString(),
661
+ oracleSeed: seedBuffer.toString('hex'),
662
+ };
663
+ }
664
+
665
+ /**
666
+ * BUILD TRANSACTION: Resolve round and pay winner
667
+ * NOTE: Always uses Round 1 PDAs due to account reuse!
668
+ */
669
+ async buildResolveRoundTransaction({ keeperAddress, roundId }) {
670
+ const [configPda] = this.getConfigPDA();
671
+ const config = await this.getConfig();
672
+
673
+ if (!config) {
674
+ throw new Error('Protocol not initialized');
675
+ }
676
+
677
+ // Always use Round 1 PDAs (account reuse!)
678
+ const [roundPda] = this.getRoundPDA(1);
679
+ const [entriesPda] = this.getEntriesPDA(1);
680
+ const keeper = new PublicKey(keeperAddress);
681
+ const actualRoundId = config.currentRoundId;
682
+
683
+ // Calculate winner from VRF result (winner is selected DURING resolve, not before!)
684
+ const roundAccount = await this.connection.getAccountInfo(roundPda);
685
+ if (!roundAccount) {
686
+ throw new Error('Round account not found');
687
+ }
688
+
689
+ // Check for VRF result at offset 51
690
+ const hasVrf = roundAccount.data[51] === 1;
691
+ if (!hasVrf) {
692
+ throw new Error('VRF result not available yet - randomness may not have been consumed');
693
+ }
694
+
695
+ // Read VRF result (u128 at offset 52-68, little-endian)
696
+ const vrfBytes = roundAccount.data.slice(52, 68);
697
+ const vrfHex = Buffer.from(vrfBytes).reverse().toString('hex');
698
+ const vrfResult = BigInt('0x' + vrfHex);
699
+
700
+ // Get entries
701
+ const entriesAccount = await this.connection.getAccountInfo(entriesPda);
702
+ if (!entriesAccount) {
703
+ throw new Error('Entries account not found');
704
+ }
705
+
706
+ const entryCount = entriesAccount.data.readUInt32LE(16);
707
+ if (entryCount === 0) {
708
+ throw new Error('No entries in round');
709
+ }
710
+
711
+ // Parse entries: discriminator(8) + round_id(8) + vec_len(4) = 20, then entries
712
+ const entries = [];
713
+ for (let i = 0; i < entryCount; i++) {
714
+ const offset = 20 + (i * 56); // Each entry is 56 bytes
715
+ const playerBytes = entriesAccount.data.slice(offset, offset + 32);
716
+ const weight = entriesAccount.data.readBigUInt64LE(offset + 32);
717
+ // cumulative_to is u128 (16 bytes), not u64!
718
+ const cumulativeBytes = entriesAccount.data.slice(offset + 40, offset + 56);
719
+ const cumulativeTo = BigInt('0x' + Buffer.from(cumulativeBytes).reverse().toString('hex'));
720
+
721
+ entries.push({
722
+ player: new PublicKey(playerBytes),
723
+ weight,
724
+ cumulativeTo
725
+ });
726
+ }
727
+
728
+ // Calculate winner using same algorithm as on-chain program:
729
+ // let winner_point = (entropy % total_weight as u128) as u64;
730
+ // if winner_point < entry.cumulative_to as u64 { ... }
731
+ const totalWeight = roundAccount.data.readBigUInt64LE(41);
732
+ const winnerPoint = BigInt.asUintN(64, vrfResult % BigInt(totalWeight));
733
+
734
+ let winner = null;
735
+ for (const entry of entries) {
736
+ if (winnerPoint < BigInt.asUintN(64, entry.cumulativeTo)) {
737
+ winner = entry.player.toString();
738
+ break;
739
+ }
740
+ }
741
+
742
+ if (!winner) {
743
+ throw new Error('Could not calculate winner - invalid cumulative weights');
744
+ }
745
+
746
+ console.log('✅ Calculated winner:', winner.slice(0, 8) + '...');
747
+
748
+ const data = DISCRIMINATORS.RESOLVE_ROUND;
749
+
750
+ const keys = [
751
+ { pubkey: configPda, isSigner: false, isWritable: true },
752
+ { pubkey: roundPda, isSigner: false, isWritable: true },
753
+ { pubkey: entriesPda, isSigner: false, isWritable: false },
754
+ { pubkey: new PublicKey(winner), isSigner: false, isWritable: true },
755
+ { pubkey: new PublicKey(config.treasury), isSigner: false, isWritable: true },
756
+ { pubkey: keeper, isSigner: true, isWritable: true },
757
+ ];
758
+
759
+ const instruction = new TransactionInstruction({
760
+ keys,
761
+ programId: this.programId,
762
+ data,
763
+ });
764
+
765
+ const transaction = new Transaction().add(instruction);
766
+ transaction.feePayer = keeper;
767
+ transaction.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash;
768
+
769
+ return {
770
+ transaction: transaction.serialize({ requireAllSignatures: false }).toString('base64'),
771
+ roundId: actualRoundId.toString(),
772
+ winner,
773
+ };
774
+ }
775
+
776
+ /**
777
+ * BUILD TRANSACTION: Reset round (account reuse like Solpot!)
778
+ * Reuses existing Round #1 accounts instead of creating new ones
779
+ */
780
+ async buildResetRoundTransaction({ keeperAddress }) {
781
+ const [configPda] = this.getConfigPDA();
782
+ const config = await this.getConfig();
783
+
784
+ if (!config) {
785
+ throw new Error('Protocol not initialized');
786
+ }
787
+
788
+ // Always use Round 1 PDAs (we reuse them forever!)
789
+ const [roundPda] = this.getRoundPDA(1);
790
+ const [entriesPda] = this.getEntriesPDA(1);
791
+ const keeper = new PublicKey(keeperAddress);
792
+
793
+ const data = DISCRIMINATORS.RESET_ROUND;
794
+
795
+ const keys = [
796
+ { pubkey: configPda, isSigner: false, isWritable: true },
797
+ { pubkey: roundPda, isSigner: false, isWritable: true },
798
+ { pubkey: entriesPda, isSigner: false, isWritable: true },
799
+ { pubkey: keeper, isSigner: true, isWritable: true },
800
+ ];
801
+
802
+ const instruction = new TransactionInstruction({
803
+ keys,
804
+ programId: this.programId,
805
+ data,
806
+ });
807
+
808
+ const transaction = new Transaction().add(instruction);
809
+ transaction.feePayer = keeper;
810
+ transaction.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash;
811
+
812
+ const nextRoundId = BigInt(config.currentRoundId) + 1n;
813
+
814
+ return {
815
+ transaction: transaction.serialize({ requireAllSignatures: false }).toString('base64'),
816
+ roundId: nextRoundId.toString(),
817
+ roundPda: roundPda.toString(),
818
+ entriesPda: entriesPda.toString(),
819
+ reused: true, // Flag that accounts were reused
820
+ };
821
+ }
822
+
823
+ /**
824
+ * Get jackpot statistics
825
+ */
826
+ async getStats() {
827
+ const config = await this.getConfig();
828
+ const currentRound = await this.getCurrentRound();
829
+
830
+ return {
831
+ initialized: !!config,
832
+ config: config || {},
833
+ currentRound: currentRound || {},
834
+ timestamp: new Date().toISOString(),
835
+ };
836
+ }
837
+
838
+ // ============================================
839
+ // HELPER FUNCTIONS
840
+ // ============================================
841
+
842
+ serializeU16(value) {
843
+ const buf = Buffer.alloc(2);
844
+ buf.writeUInt16LE(value);
845
+ return buf;
846
+ }
847
+
848
+ serializeU64(value) {
849
+ const buf = Buffer.alloc(8);
850
+ buf.writeBigUInt64LE(BigInt(value));
851
+ return buf;
852
+ }
853
+ }
854
+
855
+ module.exports = JackpotService;
856
+