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,369 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Test Script: Connect4 Game Resolution with Polling Confirmation
4
+ *
5
+ * Tests the resolveGame function in connect4GameService.js
6
+ * to verify the Alchemy-compatible polling confirmation works.
7
+ *
8
+ * Usage:
9
+ * node test-connect4-resolve.js <gameId> # Resolve specific game
10
+ * node test-connect4-resolve.js --find-pending # Find and list pending games
11
+ * node test-connect4-resolve.js --dry-run <gameId> # Show what would happen
12
+ */
13
+
14
+ require('dotenv').config();
15
+ const { Connection, Keypair, PublicKey, Transaction, TransactionInstruction } = require('@solana/web3.js');
16
+ const { pool } = require('../services/db');
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+
20
+ // Configuration
21
+ const RPC_URL = process.env.SOLANA_RPC_URL || process.env.SOLANA_NETWORK;
22
+ const PROGRAM_ID = new PublicKey(process.env.PROGRAM_ID || '85wJGp9uc8w2FeKX9CEHsudTo1UVCrmuRFy37oCcaoG1');
23
+ const RESOLVE_AUTO_DISCRIMINATOR = Buffer.from([245, 33, 115, 150, 82, 150, 28, 193]);
24
+
25
+ // Parse args
26
+ const args = process.argv.slice(2);
27
+ const options = {
28
+ gameId: null,
29
+ findPending: false,
30
+ dryRun: false,
31
+ help: false
32
+ };
33
+
34
+ args.forEach((arg, i) => {
35
+ if (arg === '--find-pending') options.findPending = true;
36
+ else if (arg === '--dry-run') {
37
+ options.dryRun = true;
38
+ options.gameId = args[i + 1];
39
+ }
40
+ else if (arg === '--help') options.help = true;
41
+ else if (!arg.startsWith('--')) options.gameId = arg;
42
+ });
43
+
44
+ console.log('๐Ÿงช Connect4 Resolution Test (with Polling Confirmation)');
45
+ console.log('========================================================');
46
+ console.log(`RPC URL: ${RPC_URL || 'NOT SET'}`);
47
+ console.log(`Program ID: ${PROGRAM_ID.toString()}`);
48
+ console.log('');
49
+
50
+ if (!RPC_URL) {
51
+ console.error('โŒ ERROR: SOLANA_RPC_URL environment variable is not set!');
52
+ console.log(' Set it to your Alchemy RPC URL');
53
+ process.exit(1);
54
+ }
55
+
56
+ const connection = new Connection(RPC_URL, 'confirmed');
57
+
58
+ /**
59
+ * Load oracle keypair
60
+ */
61
+ function loadOracleKeypair() {
62
+ if (process.env.ORACLE_WALLET_JSON) {
63
+ const secretKey = JSON.parse(process.env.ORACLE_WALLET_JSON);
64
+ return Keypair.fromSecretKey(Uint8Array.from(secretKey));
65
+ }
66
+
67
+ const walletPath = process.env.ORACLE_WALLET_PATH || path.join(__dirname, '../wallets/oracle.json');
68
+ if (fs.existsSync(walletPath)) {
69
+ const secretKey = JSON.parse(fs.readFileSync(walletPath, 'utf-8'));
70
+ return Keypair.fromSecretKey(Uint8Array.from(secretKey));
71
+ }
72
+
73
+ throw new Error('Oracle keypair not found');
74
+ }
75
+
76
+ /**
77
+ * Poll for transaction confirmation using getSignatureStatuses
78
+ * This is the Alchemy-compatible approach (no WebSocket subscriptions)
79
+ */
80
+ async function pollTransactionConfirmation(signature, lastValidBlockHeight, timeout = 30000) {
81
+ const start = Date.now();
82
+ console.log(`๐Ÿ”„ Polling confirmation for ${signature}`);
83
+ console.log(` lastValidBlockHeight: ${lastValidBlockHeight}`);
84
+
85
+ let pollCount = 0;
86
+ while (Date.now() - start < timeout) {
87
+ pollCount++;
88
+ try {
89
+ // Check if blockhash has expired
90
+ const currentBlockHeight = await connection.getBlockHeight('confirmed');
91
+ if (currentBlockHeight > lastValidBlockHeight) {
92
+ throw new Error(`Transaction expired: blockhash no longer valid (current: ${currentBlockHeight}, lastValid: ${lastValidBlockHeight})`);
93
+ }
94
+
95
+ // Poll signature status
96
+ const response = await connection.getSignatureStatuses([signature], {
97
+ searchTransactionHistory: true
98
+ });
99
+
100
+ const status = response?.value?.[0];
101
+ if (status) {
102
+ console.log(` [Poll ${pollCount}] confirmationStatus: ${status.confirmationStatus}, err: ${status.err ? JSON.stringify(status.err) : 'null'}`);
103
+
104
+ if (status.err) {
105
+ throw new Error(`Transaction failed: ${JSON.stringify(status.err)}`);
106
+ }
107
+
108
+ if (status.confirmationStatus === 'confirmed' || status.confirmationStatus === 'finalized') {
109
+ console.log(`โœ… Transaction confirmed: ${status.confirmationStatus}`);
110
+ return status;
111
+ }
112
+ } else {
113
+ console.log(` [Poll ${pollCount}] Status: null (not confirmed yet)`);
114
+ }
115
+ } catch (pollErr) {
116
+ if (pollErr.message?.includes('expired') || pollErr.message?.includes('failed')) {
117
+ throw pollErr;
118
+ }
119
+ console.warn(` [Poll ${pollCount}] Error (will retry): ${pollErr.message}`);
120
+ }
121
+
122
+ await new Promise(resolve => setTimeout(resolve, 1000));
123
+ }
124
+
125
+ throw new Error(`Transaction confirmation timeout after ${timeout}ms`);
126
+ }
127
+
128
+ /**
129
+ * Find pending Connect4 games
130
+ */
131
+ async function findPendingGames() {
132
+ console.log('๐Ÿ” Finding pending Connect4 games...\n');
133
+
134
+ const result = await pool.query(`
135
+ SELECT
136
+ g.game_id, g.game_status, g.connect4_winner, g.is_resolved, g.created_by,
137
+ g.buy_in, g.created_at,
138
+ creator.wallet_address as creator_wallet,
139
+ opponent.wallet_address as opponent_wallet
140
+ FROM games g
141
+ LEFT JOIN user_game_refs creator ON g.game_id = creator.game_id AND creator.role = 'creator'
142
+ LEFT JOIN user_game_refs opponent ON g.game_id = opponent.game_id AND opponent.role = 'opponent'
143
+ WHERE g.game_type = 'connect4'
144
+ AND g.game_status IN ('playing', 'waiting', 'pending')
145
+ AND g.is_resolved = FALSE
146
+ ORDER BY g.created_at DESC
147
+ LIMIT 10
148
+ `);
149
+
150
+ if (result.rows.length === 0) {
151
+ console.log(' No pending games found');
152
+ return [];
153
+ }
154
+
155
+ console.log(`Found ${result.rows.length} pending game(s):\n`);
156
+ result.rows.forEach(g => {
157
+ console.log(` Game ID: ${g.game_id}`);
158
+ console.log(` Status: ${g.game_status}`);
159
+ console.log(` Creator: ${g.creator_wallet?.slice(0, 16) || g.created_by?.slice(0, 16)}...`);
160
+ console.log(` Opponent: ${g.opponent_wallet?.slice(0, 16) || 'N/A'}...`);
161
+ console.log(` Buy-in: ${g.buy_in} SOL`);
162
+ console.log(` Created: ${g.created_at}`);
163
+ console.log('');
164
+ });
165
+
166
+ return result.rows;
167
+ }
168
+
169
+ /**
170
+ * Get game details
171
+ */
172
+ async function getGameDetails(gameId) {
173
+ const result = await pool.query(`
174
+ SELECT
175
+ g.game_id, g.game_status, g.connect4_winner as winner, g.is_resolved,
176
+ g.created_by, g.buy_in, g.game_address as game_pda, g.claim_signature,
177
+ creator.wallet_address as creator_wallet,
178
+ creator.claimed_at as creator_claimed_at
179
+ FROM games g
180
+ LEFT JOIN user_game_refs creator ON g.game_id = creator.game_id AND creator.role = 'creator'
181
+ WHERE g.game_id = $1
182
+ `, [gameId]);
183
+
184
+ if (result.rows.length === 0) {
185
+ throw new Error(`Game ${gameId} not found`);
186
+ }
187
+
188
+ return result.rows[0];
189
+ }
190
+
191
+ /**
192
+ * Build resolve instruction (for cancel: winner = null)
193
+ */
194
+ function buildResolveInstruction(gamePDA, gameIdBuf, winner, oracleKeypair) {
195
+ // Winner: 0 = Team A wins, 1 = Team B wins, 2 = Draw, 255 = No winner (cancel)
196
+ let winnerByte;
197
+ if (winner === null || winner === 'cancel') {
198
+ winnerByte = 255; // No winner - refund both
199
+ } else if (winner === 'creator' || winner === 'teamA' || winner === 0) {
200
+ winnerByte = 0;
201
+ } else if (winner === 'opponent' || winner === 'teamB' || winner === 1) {
202
+ winnerByte = 1;
203
+ } else if (winner === 'draw') {
204
+ winnerByte = 2;
205
+ } else {
206
+ throw new Error(`Invalid winner value: ${winner}`);
207
+ }
208
+
209
+ const data = Buffer.concat([
210
+ RESOLVE_AUTO_DISCRIMINATOR,
211
+ gameIdBuf,
212
+ Buffer.from([winnerByte])
213
+ ]);
214
+
215
+ const OPERATOR_WALLET = new PublicKey(process.env.OPERATOR_WALLET || 'BVZXwZpfgyzTBdRFHohkHZppPHnAyqyctRsKy3vWfQib');
216
+
217
+ const keys = [
218
+ { pubkey: gamePDA, isSigner: false, isWritable: true },
219
+ { pubkey: oracleKeypair.publicKey, isSigner: true, isWritable: true },
220
+ { pubkey: OPERATOR_WALLET, isSigner: false, isWritable: true },
221
+ { pubkey: require('@solana/web3.js').SystemProgram.programId, isSigner: false, isWritable: false },
222
+ ];
223
+
224
+ return new TransactionInstruction({
225
+ keys,
226
+ programId: PROGRAM_ID,
227
+ data
228
+ });
229
+ }
230
+
231
+ /**
232
+ * Resolve a Connect4 game (cancel)
233
+ */
234
+ async function resolveGame(gameId, winner = null) {
235
+ console.log(`\n๐Ÿ“‹ Resolving Connect4 Game: ${gameId}`);
236
+ console.log('------------------------------------------');
237
+
238
+ const game = await getGameDetails(gameId);
239
+ console.log(` Status: ${game.game_status}`);
240
+ console.log(` is_resolved: ${game.is_resolved}`);
241
+ console.log(` Winner: ${game.winner || 'null'}`);
242
+ console.log(` Game PDA: ${game.game_pda || 'N/A'}`);
243
+ console.log(` Creator: ${game.creator_wallet || game.created_by}`);
244
+
245
+ if (game.is_resolved) {
246
+ console.log('\nโš ๏ธ Game is already resolved!');
247
+ return null;
248
+ }
249
+
250
+ // Load oracle
251
+ const oracleKeypair = loadOracleKeypair();
252
+ console.log(` Oracle: ${oracleKeypair.publicKey.toString()}`);
253
+
254
+ // Derive game PDA if not stored
255
+ let gamePDA;
256
+ if (game.game_pda) {
257
+ gamePDA = new PublicKey(game.game_pda);
258
+ } else {
259
+ // Derive from game_id
260
+ const gameIdNum = BigInt(gameId.replace('connect4-', '').split('-')[0]);
261
+ const gameIdBuf = Buffer.alloc(8);
262
+ gameIdBuf.writeBigUInt64LE(gameIdNum);
263
+
264
+ [gamePDA] = PublicKey.findProgramAddressSync(
265
+ [Buffer.from('game'), gameIdBuf],
266
+ PROGRAM_ID
267
+ );
268
+ }
269
+ console.log(` Derived PDA: ${gamePDA.toString()}`);
270
+
271
+ // Check if account exists on-chain
272
+ const accountInfo = await connection.getAccountInfo(gamePDA);
273
+ if (!accountInfo) {
274
+ console.log('\nโŒ Game account not found on-chain!');
275
+ return null;
276
+ }
277
+ console.log(` On-chain account: ${accountInfo.data.length} bytes`);
278
+
279
+ // Build game ID buffer
280
+ const gameIdNum = BigInt(gameId.replace('connect4-', '').split('-')[0]);
281
+ const gameIdBuf = Buffer.alloc(8);
282
+ gameIdBuf.writeBigUInt64LE(gameIdNum);
283
+
284
+ // Build instruction
285
+ const ix = buildResolveInstruction(gamePDA, gameIdBuf, winner, oracleKeypair);
286
+
287
+ // Build transaction
288
+ const tx = new Transaction().add(ix);
289
+ const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash('confirmed');
290
+ tx.recentBlockhash = blockhash;
291
+ tx.feePayer = oracleKeypair.publicKey;
292
+ tx.sign(oracleKeypair);
293
+
294
+ console.log(`\n๐Ÿ“ก Sending resolve transaction...`);
295
+ console.log(` Blockhash: ${blockhash}`);
296
+ console.log(` lastValidBlockHeight: ${lastValidBlockHeight}`);
297
+
298
+ const signature = await connection.sendRawTransaction(tx.serialize());
299
+ console.log(` Signature: ${signature}`);
300
+ console.log(` https://solscan.io/tx/${signature}`);
301
+
302
+ // Use polling confirmation (Alchemy-compatible)
303
+ console.log(`\nโณ Waiting for confirmation (polling)...`);
304
+ try {
305
+ await pollTransactionConfirmation(signature, lastValidBlockHeight, 30000);
306
+ console.log(`\nโœ… Game resolved successfully!`);
307
+
308
+ // Update database
309
+ await pool.query(`
310
+ UPDATE games SET
311
+ is_resolved = TRUE,
312
+ game_status = 'cancelled',
313
+ connect4_winner = NULL,
314
+ claim_signature = $1,
315
+ resolved_at = NOW(),
316
+ updated_at = NOW()
317
+ WHERE game_id = $2
318
+ `, [signature, gameId]);
319
+
320
+ console.log(` Database updated`);
321
+ return signature;
322
+
323
+ } catch (confirmErr) {
324
+ if (confirmErr.message?.includes('AlreadyResolved')) {
325
+ console.log(`\nโš ๏ธ Game was already resolved on-chain`);
326
+ return null;
327
+ }
328
+ throw confirmErr;
329
+ }
330
+ }
331
+
332
+ /**
333
+ * Main
334
+ */
335
+ async function main() {
336
+ if (options.help) {
337
+ console.log('Usage:');
338
+ console.log(' node test-connect4-resolve.js <gameId> # Resolve (cancel) game');
339
+ console.log(' node test-connect4-resolve.js --find-pending # List pending games');
340
+ console.log(' node test-connect4-resolve.js --dry-run <gameId> # Show details only');
341
+ process.exit(0);
342
+ }
343
+
344
+ try {
345
+ if (options.findPending) {
346
+ await findPendingGames();
347
+ } else if (options.dryRun && options.gameId) {
348
+ console.log('DRY RUN - showing game details only\n');
349
+ const game = await getGameDetails(options.gameId);
350
+ console.log(JSON.stringify(game, null, 2));
351
+ } else if (options.gameId) {
352
+ await resolveGame(options.gameId, null); // null = cancel
353
+ } else {
354
+ console.log('No game ID provided. Use --find-pending to list games.');
355
+ }
356
+
357
+ } catch (error) {
358
+ console.error('\nโŒ Error:', error.message);
359
+ if (error.logs) {
360
+ console.error('Program logs:');
361
+ error.logs.forEach(log => console.error(` ${log}`));
362
+ }
363
+ process.exit(1);
364
+ } finally {
365
+ await pool.end();
366
+ }
367
+ }
368
+
369
+ main();
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Test Game State Endpoint
4
+ * Tests the /api/games/:gameId/state endpoint for different users
5
+ */
6
+
7
+ require('dotenv').config();
8
+ const axios = require('axios');
9
+ const https = require('https');
10
+
11
+ // Create axios instance that ignores SSL cert issues (for testing only!)
12
+ const axiosInstance = axios.create({
13
+ httpsAgent: new https.Agent({
14
+ rejectUnauthorized: false
15
+ })
16
+ });
17
+
18
+ const API_URL = process.env.DUBS_SERVER_URL || 'http://localhost:3001';
19
+
20
+ // Test game ID from database
21
+ const TEST_GAME_ID = 'sport-1764007906160-dnxc6zbps';
22
+
23
+ // Test wallets - all 3 joined away team and WON
24
+ const TEST_WALLETS = {
25
+ creator: '3sgQXvLKs4XfBTxG3jY2J2E5NougemfC6i4nZbUVx8xt', // Away team (WINNER)
26
+ joiner1: 'E2SXYN3GVvQyEo1Pei8PUsyTmFqgkfuK7jTau9CrpfYC', // Away team (WINNER)
27
+ joiner2: 'CMt4nTCGmTXu7LsH5ujsnUsnBpb6qk7hQJAUPvHoW4aD', // Away team (WINNER)
28
+ nonParticipant: 'NonParticipantWallet123'
29
+ };
30
+
31
+ async function testGameState(walletAddress, label) {
32
+ console.log(`\n${'='.repeat(60)}`);
33
+ console.log(`Testing: ${label}`);
34
+ console.log(`Wallet: ${walletAddress}`);
35
+ console.log('='.repeat(60));
36
+
37
+ try {
38
+ const response = await axiosInstance.get(
39
+ `${API_URL}/api/games/${TEST_GAME_ID}/state`,
40
+ {
41
+ headers: {
42
+ 'x-wallet-address': walletAddress
43
+ }
44
+ }
45
+ );
46
+
47
+ console.log('\nโœ… Response Status:', response.status);
48
+ console.log('\n๐Ÿ“Š Game State:');
49
+ console.log(JSON.stringify(response.data, null, 2));
50
+
51
+ // Validate expected fields
52
+ const state = response.data;
53
+ console.log('\n๐Ÿ” Validation:');
54
+ console.log(` isLocked: ${state.isLocked} ${state.isLocked !== undefined ? 'โœ…' : 'โŒ'}`);
55
+ console.log(` isResolved: ${state.isResolved} ${state.isResolved !== undefined ? 'โœ…' : 'โŒ'}`);
56
+ console.log(` totalPlayers: ${state.totalPlayers} ${state.totalPlayers !== undefined ? 'โœ…' : 'โŒ'}`);
57
+
58
+ if (state.isResolved) {
59
+ console.log(` winner: ${state.winner} ${state.winner ? 'โœ…' : 'โŒ'}`);
60
+ console.log(` homeScore: ${state.homeScore} ${state.homeScore !== undefined ? 'โœ…' : 'โŒ'}`);
61
+ console.log(` awayScore: ${state.awayScore} ${state.awayScore !== undefined ? 'โœ…' : 'โŒ'}`);
62
+ }
63
+
64
+ console.log(` userParticipated: ${state.userParticipated} ${state.userParticipated !== undefined ? 'โœ…' : 'โŒ'}`);
65
+
66
+ if (state.userParticipated) {
67
+ console.log(` userTeamChoice: ${state.userTeamChoice} ${state.userTeamChoice ? 'โœ…' : 'โŒ'}`);
68
+ console.log(` userWon: ${state.userWon} ${state.userWon !== undefined ? 'โœ…' : 'โŒ'}`);
69
+ }
70
+
71
+ // Determine expected button state
72
+ console.log('\n๐ŸŽฎ Expected Button State:');
73
+ if (state.isResolved) {
74
+ if (state.userParticipated) {
75
+ if (state.userWon) {
76
+ console.log(' โœ… Should show: "๐Ÿ† Claim Prize" (green, pulsing)');
77
+ } else {
78
+ console.log(' โŒ Should show: "โœ–๏ธ You Lost" (red)');
79
+ }
80
+ } else {
81
+ console.log(' ๐Ÿ‘๏ธ Should show: "View Results" (grey)');
82
+ }
83
+ } else if (state.isLocked) {
84
+ console.log(' ๐Ÿ”’ Should show: "Game Started" (grey, disabled)');
85
+ } else {
86
+ console.log(' ๐Ÿ† Should show: "Join Bet" (gradient, clickable)');
87
+ }
88
+
89
+ } catch (error) {
90
+ console.error('\nโŒ Error:', error.message);
91
+ if (error.response) {
92
+ console.error('Status:', error.response.status);
93
+ console.error('Data:', error.response.data);
94
+ }
95
+ }
96
+ }
97
+
98
+ async function runTests() {
99
+ console.log('๐Ÿงช Testing Game State Endpoint');
100
+ console.log(`API URL: ${API_URL}`);
101
+ console.log(`Game ID: ${TEST_GAME_ID}`);
102
+
103
+ // Test 1: Creator (who participated)
104
+ await testGameState(TEST_WALLETS.creator, 'Creator/Participant');
105
+
106
+ // Test 2: Joiner 1
107
+ await testGameState(TEST_WALLETS.joiner1, 'Joiner #1');
108
+
109
+ // Test 3: Joiner 2
110
+ await testGameState(TEST_WALLETS.joiner2, 'Joiner #2');
111
+
112
+ // Test 4: Non-participant
113
+ await testGameState(TEST_WALLETS.nonParticipant, 'Non-Participant (Spectator)');
114
+
115
+ // Test 5: No wallet header
116
+ console.log(`\n${'='.repeat(60)}`);
117
+ console.log('Testing: No Wallet Header (Public Access)');
118
+ console.log('='.repeat(60));
119
+
120
+ try {
121
+ const response = await axiosInstance.get(`${API_URL}/api/games/${TEST_GAME_ID}/state`);
122
+ console.log('\nโœ… Response Status:', response.status);
123
+ console.log('\n๐Ÿ“Š Game State:');
124
+ console.log(JSON.stringify(response.data, null, 2));
125
+ console.log('\n๐Ÿ” Should NOT include: userParticipated, userWon, userTeamChoice');
126
+ } catch (error) {
127
+ console.error('\nโŒ Error:', error.message);
128
+ }
129
+
130
+ console.log('\n' + '='.repeat(60));
131
+ console.log('โœ… Tests Complete!');
132
+ console.log('='.repeat(60));
133
+ }
134
+
135
+ runTests().catch(console.error);
136
+
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Test script for game invite notifications with CTA button
3
+ *
4
+ * Usage: node test-invite-notification.js <gameId> <telegramUserId> [botToken]
5
+ *
6
+ * Example:
7
+ * node test-invite-notification.js sport-1766923697972-8viqv89wv 7874505364
8
+ */
9
+
10
+ require('dotenv').config();
11
+
12
+ const GAME_ID = process.argv[2];
13
+ const TELEGRAM_USER_ID = process.argv[3];
14
+ const BOT_TOKEN = process.argv[4] || process.env.TELEGRAM_BOT_TOKEN;
15
+
16
+ if (!GAME_ID || !TELEGRAM_USER_ID) {
17
+ console.error('Usage: node test-invite-notification.js <gameId> <telegramUserId> [botToken]');
18
+ console.error('Example: node test-invite-notification.js sport-1766923697972-8viqv89wv 7874505364');
19
+ process.exit(1);
20
+ }
21
+
22
+ if (!BOT_TOKEN) {
23
+ console.error('โŒ No bot token provided. Either set TELEGRAM_BOT_TOKEN in .env or pass as 3rd argument.');
24
+ process.exit(1);
25
+ }
26
+
27
+ const axios = require('axios');
28
+ const urlHelper = require('../utils/urlHelper');
29
+
30
+ const TELEGRAM_API = `https://api.telegram.org/bot${BOT_TOKEN}`;
31
+
32
+ async function sendTestInviteNotification() {
33
+ console.log('\n๐Ÿงช Test Game Invite Notification');
34
+ console.log('='.repeat(40));
35
+ console.log(`๐Ÿ“Œ Game ID: ${GAME_ID}`);
36
+ console.log(`๐Ÿ‘ค Telegram User ID: ${TELEGRAM_USER_ID}`);
37
+ console.log(`๐ŸŒ Environment: ${process.env.NODE_ENV || 'development'}`);
38
+
39
+ const joinUrl = urlHelper.getGameShareUrl(GAME_ID);
40
+ console.log(`\n๐Ÿ”— Join URL: ${joinUrl}`);
41
+
42
+ const inviterUsername = '78naim';
43
+ const gameTitle = 'Los Angeles Rams @ Atlanta Falcons';
44
+ const buyIn = '0.1';
45
+
46
+ const notificationText = `๐Ÿ”” *Notification*\n\n๐ŸŽฎ ${inviterUsername} invited you to join their bet!\n\n${inviterUsername} invited you to join their ${gameTitle} bet! ${buyIn} SOL buy-in`;
47
+
48
+ const payload = {
49
+ chat_id: TELEGRAM_USER_ID,
50
+ text: notificationText,
51
+ parse_mode: 'Markdown',
52
+ disable_web_page_preview: true,
53
+ reply_markup: {
54
+ inline_keyboard: [[
55
+ { text: '๐ŸŽฎ Join Game', url: joinUrl }
56
+ ]]
57
+ }
58
+ };
59
+
60
+ try {
61
+ console.log('\n๐Ÿ“ค Sending notification...');
62
+ const response = await axios.post(`${TELEGRAM_API}/sendMessage`, payload);
63
+
64
+ if (response.data.ok) {
65
+ console.log(`\nโœ… Notification sent successfully!`);
66
+ console.log(`๐Ÿ“ฌ Message ID: ${response.data.result.message_id}`);
67
+ console.log(`\n๐Ÿ“ฑ Next steps:
68
+ 1. Check your Telegram for the notification
69
+ 2. You should see a "๐ŸŽฎ Join Game" button
70
+ 3. Tap the button to test the deferred deep link
71
+ 4. Verify it takes you to the game join page
72
+ `);
73
+ } else {
74
+ console.error('โŒ Telegram API returned not OK:', response.data);
75
+ }
76
+ } catch (error) {
77
+ if (error.response) {
78
+ console.error(`โŒ Telegram API error (${error.response.status}):`, error.response.data?.description || error.message);
79
+ } else {
80
+ console.error('โŒ Failed to send:', error.message);
81
+ }
82
+ }
83
+ }
84
+
85
+ sendTestInviteNotification();
86
+
@@ -0,0 +1,71 @@
1
+ #!/bin/bash
2
+
3
+ # ๐ŸŽฐ Test script for Jackpot API
4
+ # Tests all endpoints to ensure they're working
5
+
6
+ BASE_URL="http://localhost:3001/jackpot"
7
+
8
+ echo "๐ŸŽฐ Testing Dubs Jackpot API"
9
+ echo "============================="
10
+ echo ""
11
+
12
+ # Colors
13
+ GREEN='\033[0;32m'
14
+ BLUE='\033[0;34m'
15
+ YELLOW='\033[1;33m'
16
+ RED='\033[0;31m'
17
+ NC='\033[0m'
18
+
19
+ # Test health endpoint
20
+ echo -e "${BLUE}1. Testing health endpoint...${NC}"
21
+ curl -s $BASE_URL/health | jq '.'
22
+ echo ""
23
+
24
+ # Test config endpoint
25
+ echo -e "${BLUE}2. Testing config endpoint...${NC}"
26
+ curl -s $BASE_URL/config | jq '.'
27
+ echo ""
28
+
29
+ # Test current round
30
+ echo -e "${BLUE}3. Testing current round endpoint...${NC}"
31
+ curl -s $BASE_URL/round/current | jq '.'
32
+ echo ""
33
+
34
+ # Test stats endpoint
35
+ echo -e "${BLUE}4. Testing stats endpoint...${NC}"
36
+ curl -s $BASE_URL/stats | jq '.'
37
+ echo ""
38
+
39
+ # Test building an enter transaction
40
+ echo -e "${BLUE}5. Testing build enter transaction...${NC}"
41
+ curl -s -X POST $BASE_URL/build/enter \
42
+ -H "Content-Type: application/json" \
43
+ -d '{
44
+ "playerAddress": "57voP1Y8U4ztX2YAcHveK3JFvVRn1n6T6iUnHdAW6xr9",
45
+ "amount": 100000000
46
+ }' | jq '.transaction | length'
47
+ echo " characters in transaction"
48
+ echo ""
49
+
50
+ # Test building open round transaction
51
+ echo -e "${BLUE}6. Testing build open-round transaction...${NC}"
52
+ curl -s -X POST $BASE_URL/build/open-round \
53
+ -H "Content-Type: application/json" \
54
+ -d '{
55
+ "keeperAddress": "57voP1Y8U4ztX2YAcHveK3JFvVRn1n6T6iUnHdAW6xr9"
56
+ }' | jq '.roundId, .roundPda'
57
+ echo ""
58
+
59
+ echo -e "${GREEN}โœ… API Test Complete!${NC}"
60
+ echo ""
61
+ echo "๐Ÿ“š Full API docs: JACKPOT_API.md"
62
+ echo "๐ŸŽฐ Program ID: bqoSjTSPLweMuqNG6jy39jmzGyZvZdWjsr4csGfD8F6"
63
+ echo ""
64
+ echo "๐Ÿš€ Start server: node server.js"
65
+ echo "๐Ÿ’ป Access at: http://localhost:3001/jackpot"
66
+
67
+
68
+
69
+
70
+
71
+