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,217 @@
1
+ /**
2
+ * Portfolio API Routes
3
+ *
4
+ * Public endpoints for fetching Solana wallet portfolios (SOL + SPL tokens).
5
+ * Data is fetched directly from Solana RPC with 30-second caching.
6
+ * Also includes admin endpoints for browsing all users' portfolios.
7
+ */
8
+
9
+ const express = require('express');
10
+ const router = express.Router();
11
+ const redisService = require('../services/redisService');
12
+
13
+ // Cache key for users list
14
+ const USERS_CACHE_KEY = 'portfolio:users:list';
15
+ const USERS_CACHE_TTL = 60; // 1 minute TTL for users list
16
+
17
+ module.exports = (portfolioService, dbPool) => {
18
+ /**
19
+ * GET /api/portfolio/health
20
+ * Health check endpoint for the portfolio service
21
+ * NOTE: Must be defined BEFORE /:walletAddress to avoid route collision
22
+ */
23
+ router.get('/health', async (req, res) => {
24
+ const stats = portfolioService.getCacheStats();
25
+ res.json({
26
+ success: true,
27
+ status: 'healthy',
28
+ cache: stats,
29
+ });
30
+ });
31
+
32
+ /**
33
+ * GET /api/portfolio/users
34
+ * Get list of all users with wallet addresses (for admin portfolio browsing)
35
+ * Returns: id, wallet_address, username, avatar
36
+ * Cached in Redis for 1 minute
37
+ * NOTE: Must be defined BEFORE /:walletAddress to avoid route collision
38
+ */
39
+ router.get('/users', async (req, res) => {
40
+ try {
41
+ // Check Redis cache first
42
+ if (redisService.isAvailable()) {
43
+ try {
44
+ const cached = await redisService.get(USERS_CACHE_KEY);
45
+ if (cached) {
46
+ const data = JSON.parse(cached);
47
+ return res.json({
48
+ success: true,
49
+ data: data.users,
50
+ totalUsers: data.totalUsers,
51
+ fromCache: true,
52
+ cachedAt: data.cachedAt,
53
+ });
54
+ }
55
+ } catch (e) {
56
+ console.error('[Portfolio] Redis cache read error:', e.message);
57
+ }
58
+ }
59
+
60
+ // Fetch from database
61
+ const result = await dbPool.query(`
62
+ SELECT
63
+ id,
64
+ wallet_address,
65
+ username,
66
+ avatar,
67
+ created_at
68
+ FROM users
69
+ WHERE wallet_address IS NOT NULL
70
+ ORDER BY created_at DESC
71
+ `);
72
+
73
+ const users = result.rows.map(row => ({
74
+ id: row.id,
75
+ walletAddress: row.wallet_address,
76
+ username: row.username || null,
77
+ avatar: row.avatar || null,
78
+ createdAt: row.created_at,
79
+ }));
80
+
81
+ const cacheData = {
82
+ users,
83
+ totalUsers: users.length,
84
+ cachedAt: new Date().toISOString(),
85
+ };
86
+
87
+ // Cache in Redis
88
+ if (redisService.isAvailable()) {
89
+ try {
90
+ await redisService.set(USERS_CACHE_KEY, JSON.stringify(cacheData), USERS_CACHE_TTL);
91
+ } catch (e) {
92
+ console.error('[Portfolio] Redis cache write error:', e.message);
93
+ }
94
+ }
95
+
96
+ res.json({
97
+ success: true,
98
+ data: users,
99
+ totalUsers: users.length,
100
+ fromCache: false,
101
+ });
102
+ } catch (error) {
103
+ console.error('[Portfolio] Error fetching users:', error.message);
104
+ res.status(500).json({
105
+ success: false,
106
+ error: 'Failed to fetch users list',
107
+ });
108
+ }
109
+ });
110
+
111
+ /**
112
+ * GET /api/portfolio/:walletAddress
113
+ * Get full portfolio for a wallet (SOL balance + all SPL tokens)
114
+ *
115
+ * Query params:
116
+ * - network: 'mainnet-beta' | 'devnet' (default: mainnet-beta)
117
+ *
118
+ * Response:
119
+ * {
120
+ * success: true,
121
+ * data: {
122
+ * walletAddress: "8syiXBXMF55...",
123
+ * network: "mainnet-beta",
124
+ * nativeBalance: { symbol: "SOL", balance: 1.5, ... },
125
+ * tokens: [...],
126
+ * totalTokens: 5,
127
+ * cachedAt: "2026-02-01T...",
128
+ * fromCache: true/false
129
+ * }
130
+ * }
131
+ */
132
+ router.get('/:walletAddress', async (req, res) => {
133
+ try {
134
+ const { walletAddress } = req.params;
135
+ const network = req.query.network || 'mainnet-beta';
136
+
137
+ // Validate network parameter
138
+ const validNetworks = ['mainnet-beta', 'devnet'];
139
+ if (!validNetworks.includes(network)) {
140
+ return res.status(400).json({
141
+ success: false,
142
+ error: `Invalid network. Must be one of: ${validNetworks.join(', ')}`,
143
+ });
144
+ }
145
+
146
+ const portfolio = await portfolioService.getPortfolio(walletAddress, network, false);
147
+
148
+ res.json({
149
+ success: true,
150
+ data: portfolio,
151
+ });
152
+ } catch (error) {
153
+ console.error('[Portfolio] Error:', error.message);
154
+
155
+ // Handle specific error types
156
+ if (error.message === 'Invalid wallet address') {
157
+ return res.status(400).json({
158
+ success: false,
159
+ error: 'Invalid wallet address format',
160
+ });
161
+ }
162
+
163
+ res.status(500).json({
164
+ success: false,
165
+ error: error.message || 'Failed to fetch portfolio',
166
+ });
167
+ }
168
+ });
169
+
170
+ /**
171
+ * GET /api/portfolio/:walletAddress/refresh
172
+ * Force cache refresh and return fresh portfolio data
173
+ *
174
+ * Query params:
175
+ * - network: 'mainnet-beta' | 'devnet' (default: mainnet-beta)
176
+ */
177
+ router.get('/:walletAddress/refresh', async (req, res) => {
178
+ try {
179
+ const { walletAddress } = req.params;
180
+ const network = req.query.network || 'mainnet-beta';
181
+
182
+ // Validate network parameter
183
+ const validNetworks = ['mainnet-beta', 'devnet'];
184
+ if (!validNetworks.includes(network)) {
185
+ return res.status(400).json({
186
+ success: false,
187
+ error: `Invalid network. Must be one of: ${validNetworks.join(', ')}`,
188
+ });
189
+ }
190
+
191
+ // Force refresh by passing true
192
+ const portfolio = await portfolioService.getPortfolio(walletAddress, network, true);
193
+
194
+ res.json({
195
+ success: true,
196
+ data: portfolio,
197
+ refreshed: true,
198
+ });
199
+ } catch (error) {
200
+ console.error('[Portfolio] Refresh error:', error.message);
201
+
202
+ if (error.message === 'Invalid wallet address') {
203
+ return res.status(400).json({
204
+ success: false,
205
+ error: 'Invalid wallet address format',
206
+ });
207
+ }
208
+
209
+ res.status(500).json({
210
+ success: false,
211
+ error: error.message || 'Failed to refresh portfolio',
212
+ });
213
+ }
214
+ });
215
+
216
+ return router;
217
+ };
@@ -0,0 +1,418 @@
1
+ /**
2
+ * Promo Routes - Handle promo code validation and sponsored game joins
3
+ *
4
+ * Endpoints:
5
+ * POST /promo/validate - Validate a promo code (check if available)
6
+ * POST /promo/reserve - Reserve a promo code for a user
7
+ * GET /promo/active - Get user's active promo code
8
+ * GET /promo/status - Get treasury status (admin)
9
+ * GET /promo/stats - Get promo code statistics (admin)
10
+ * POST /promo/generate - Generate new promo codes (admin)
11
+ */
12
+
13
+ const express = require('express');
14
+ const router = express.Router();
15
+ const { authenticate } = require('../middleware/authenticate');
16
+ const promoService = require('../services/promoService');
17
+ const promoTreasuryService = require('../services/promoTreasuryService');
18
+
19
+ // Admin wallet addresses that can manage promo codes
20
+ const ADMIN_WALLETS = [
21
+ 'Hvv1ctqHLR5wonuuRguefS6EpGUe7tFRBX2YWHGr3mes', // Main admin
22
+ '27MAuzKZT5SfE6iwLJaaBpzcXeD474ybDsJEv4Fp7Cmj',
23
+ ];
24
+
25
+ /**
26
+ * Middleware: Require admin wallet for sensitive operations
27
+ */
28
+ function requireAdmin(req, res, next) {
29
+ if (!req.user || !ADMIN_WALLETS.includes(req.user.walletAddress)) {
30
+ return res.status(403).json({
31
+ success: false,
32
+ error: 'Admin access required',
33
+ code: 'ADMIN_REQUIRED',
34
+ });
35
+ }
36
+ next();
37
+ }
38
+
39
+ /**
40
+ * POST /promo/validate
41
+ * Validate a promo code (check if it exists and is available)
42
+ * Does NOT reserve the code - just checks validity
43
+ *
44
+ * Body: { code: "DUBS-XXXX-XXXX" }
45
+ * Response: { valid: boolean, amountSOL?: number, error?: string }
46
+ */
47
+ router.post('/validate', async (req, res) => {
48
+ try {
49
+ const { code } = req.body;
50
+
51
+ if (!code) {
52
+ return res.status(400).json({ valid: false, error: 'Promo code is required' });
53
+ }
54
+
55
+ const result = await promoService.validateCode(code);
56
+ res.json(result);
57
+
58
+ } catch (error) {
59
+ console.error('[PromoRoutes] Error validating code:', error.message);
60
+ res.status(500).json({ valid: false, error: 'Server error validating promo code' });
61
+ }
62
+ });
63
+
64
+ /**
65
+ * POST /promo/reserve
66
+ * Reserve a promo code for a user during onboarding
67
+ * Code is reserved for 30 minutes before expiring
68
+ *
69
+ * Body: { code: "DUBS-XXXX-XXXX", walletAddress: "..." }
70
+ * Response: { success: boolean, amountSOL?: number, expiresAt?: string, error?: string }
71
+ */
72
+ router.post('/reserve', async (req, res) => {
73
+ try {
74
+ const { code, walletAddress } = req.body;
75
+
76
+ if (!code) {
77
+ return res.status(400).json({ success: false, error: 'Promo code is required' });
78
+ }
79
+
80
+ if (!walletAddress) {
81
+ return res.status(400).json({ success: false, error: 'Wallet address is required' });
82
+ }
83
+
84
+ // Check treasury balance before allowing reservation
85
+ const treasuryReady = promoTreasuryService.isReady();
86
+ if (!treasuryReady) {
87
+ return res.status(503).json({
88
+ success: false,
89
+ error: 'Promo system is currently unavailable'
90
+ });
91
+ }
92
+
93
+ const result = await promoService.reserveCode(code, walletAddress);
94
+
95
+ if (result.success) {
96
+ // Verify treasury has enough balance for this promo amount
97
+ const balanceCheck = await promoTreasuryService.hasEnoughBalance(result.amountLamports);
98
+ if (!balanceCheck.enough) {
99
+ // Release the reservation - treasury can't cover it
100
+ console.error('[PromoRoutes] Treasury balance insufficient, releasing reservation');
101
+ // Note: We don't have an explicit "release" function, but the reservation
102
+ // will auto-expire. For better UX, we could add one.
103
+ return res.status(503).json({
104
+ success: false,
105
+ error: 'Promo system temporarily unavailable - please try again later'
106
+ });
107
+ }
108
+ }
109
+
110
+ res.json(result);
111
+
112
+ } catch (error) {
113
+ console.error('[PromoRoutes] Error reserving code:', error.message);
114
+ res.status(500).json({ success: false, error: 'Server error reserving promo code' });
115
+ }
116
+ });
117
+
118
+ /**
119
+ * GET /promo/active
120
+ * Get user's currently active (reserved but unused) promo code
121
+ *
122
+ * Query: ?walletAddress=...
123
+ * Response: { code?: string, amountSOL?: number, expiresAt?: string } or null
124
+ */
125
+ router.get('/active', async (req, res) => {
126
+ try {
127
+ const { walletAddress } = req.query;
128
+
129
+ if (!walletAddress) {
130
+ return res.status(400).json({ error: 'Wallet address is required' });
131
+ }
132
+
133
+ const activePromo = await promoService.getActivePromoForUser(walletAddress);
134
+ res.json(activePromo || { hasActivePromo: false });
135
+
136
+ } catch (error) {
137
+ console.error('[PromoRoutes] Error getting active promo:', error.message);
138
+ res.status(500).json({ error: 'Server error getting active promo' });
139
+ }
140
+ });
141
+
142
+ /**
143
+ * GET /promo/status
144
+ * Get treasury wallet status (for admin monitoring)
145
+ * PROTECTED: Admin only
146
+ *
147
+ * Response: { initialized: boolean, address?: string, balance?: { sol: number, isLow: boolean } }
148
+ */
149
+ router.get('/status', authenticate, requireAdmin, async (req, res) => {
150
+ try {
151
+ const status = await promoTreasuryService.getStatus();
152
+ res.json(status);
153
+
154
+ } catch (error) {
155
+ console.error('[PromoRoutes] Error getting treasury status:', error.message);
156
+ res.status(500).json({ error: 'Server error getting treasury status' });
157
+ }
158
+ });
159
+
160
+ /**
161
+ * GET /promo/stats
162
+ * Get promo code statistics (for admin dashboard)
163
+ * PROTECTED: Admin only
164
+ *
165
+ * Response: { totalCodes, available, reserved, used, outcomes: { won, lost, refunded }, totalUsedSOL }
166
+ */
167
+ router.get('/stats', authenticate, requireAdmin, async (req, res) => {
168
+ try {
169
+ const stats = await promoService.getStats();
170
+ res.json(stats);
171
+
172
+ } catch (error) {
173
+ console.error('[PromoRoutes] Error getting stats:', error.message);
174
+ res.status(500).json({ error: 'Server error getting promo stats' });
175
+ }
176
+ });
177
+
178
+ /**
179
+ * GET /promo/treasury
180
+ * Get treasury wallet balance (for admin dashboard)
181
+ * PROTECTED: Admin only
182
+ *
183
+ * Response: { address, balanceLamports, balanceSOL, network }
184
+ */
185
+ router.get('/treasury', authenticate, requireAdmin, async (req, res) => {
186
+ try {
187
+ const treasuryInfo = await promoTreasuryService.getTreasuryInfo();
188
+ res.json(treasuryInfo);
189
+
190
+ } catch (error) {
191
+ console.error('[PromoRoutes] Error getting treasury info:', error.message);
192
+ res.status(500).json({ error: 'Server error getting treasury info' });
193
+ }
194
+ });
195
+
196
+ /**
197
+ * POST /promo/generate
198
+ * Generate new promo codes (admin only)
199
+ * PROTECTED: Admin only
200
+ *
201
+ * Body: { count: number, amountSOL?: number, expiresAt?: string, batchId?: string }
202
+ * Response: { codes: [{ code, amountSOL }], batchId }
203
+ */
204
+ router.post('/generate', authenticate, requireAdmin, async (req, res) => {
205
+ try {
206
+ const { count = 2, amountSOL, expiresAt, batchId, notes } = req.body;
207
+
208
+ if (count < 1 || count > 100) {
209
+ return res.status(400).json({ error: 'Count must be between 1 and 100' });
210
+ }
211
+
212
+ const options = {
213
+ batchId: batchId || `twitter-${new Date().toISOString().split('T')[0]}`,
214
+ notes
215
+ };
216
+
217
+ if (amountSOL) {
218
+ options.amountLamports = Math.floor(amountSOL * 1_000_000_000);
219
+ }
220
+
221
+ if (expiresAt) {
222
+ options.expiresAt = new Date(expiresAt);
223
+ }
224
+
225
+ const codes = await promoService.generateBatch(count, options);
226
+
227
+ res.json({
228
+ success: true,
229
+ batchId: options.batchId,
230
+ count: codes.length,
231
+ codes
232
+ });
233
+
234
+ } catch (error) {
235
+ console.error('[PromoRoutes] Error generating codes:', error.message);
236
+ res.status(500).json({ error: 'Server error generating promo codes' });
237
+ }
238
+ });
239
+
240
+ /**
241
+ * GET /promo/all
242
+ * Get all promo codes (admin only)
243
+ * PROTECTED: Admin only
244
+ *
245
+ * Response: { codes: [...] }
246
+ */
247
+ router.get('/all', authenticate, requireAdmin, async (req, res) => {
248
+ try {
249
+ const codes = await promoService.getAllCodes();
250
+ res.json({ codes });
251
+
252
+ } catch (error) {
253
+ console.error('[PromoRoutes] Error getting all codes:', error.message);
254
+ res.status(500).json({ error: 'Server error getting promo codes' });
255
+ }
256
+ });
257
+
258
+ /**
259
+ * GET /promo/batch/:batchId
260
+ * Get all codes in a batch (for tracking Twitter giveaways)
261
+ * PROTECTED: Admin only
262
+ *
263
+ * Response: { codes: [...] }
264
+ */
265
+ router.get('/batch/:batchId', authenticate, requireAdmin, async (req, res) => {
266
+ try {
267
+ const { batchId } = req.params;
268
+
269
+ const codes = await promoService.getCodesByBatch(batchId);
270
+ res.json({ batchId, count: codes.length, codes });
271
+
272
+ } catch (error) {
273
+ console.error('[PromoRoutes] Error getting batch:', error.message);
274
+ res.status(500).json({ error: 'Server error getting batch codes' });
275
+ }
276
+ });
277
+
278
+ /**
279
+ * GET /promo/status/:walletAddress
280
+ * Get user's promo status for the in-app banner
281
+ * Returns info about their reserved or used promo code
282
+ *
283
+ * Response: {
284
+ * hasActivePromo: boolean,
285
+ * code: string | null,
286
+ * amountSOL: number,
287
+ * status: 'reserved' | 'used' | null,
288
+ * gameOutcome: 'won' | 'lost' | 'refunded' | null,
289
+ * bannerDismissed: boolean
290
+ * }
291
+ */
292
+ router.get('/status/:walletAddress', async (req, res) => {
293
+ try {
294
+ const { walletAddress } = req.params;
295
+
296
+ if (!walletAddress) {
297
+ return res.status(400).json({ error: 'Wallet address is required' });
298
+ }
299
+
300
+ const status = await promoService.getUserPromoStatus(walletAddress);
301
+ res.json(status);
302
+
303
+ } catch (error) {
304
+ console.error('[PromoRoutes] Error getting user promo status:', error.message);
305
+ res.status(500).json({ error: 'Server error getting promo status' });
306
+ }
307
+ });
308
+
309
+ /**
310
+ * POST /promo/release
311
+ * Release a promo code back to available status (admin only)
312
+ * Use this to clean up duplicate reservations
313
+ * PROTECTED: Admin only
314
+ *
315
+ * Body: { code: "DUBS-XXXX-XXXX" }
316
+ * Response: { success: boolean, code?: string, error?: string }
317
+ */
318
+ router.post('/release', authenticate, requireAdmin, async (req, res) => {
319
+ try {
320
+ const { code } = req.body;
321
+
322
+ if (!code) {
323
+ return res.status(400).json({ success: false, error: 'Code is required' });
324
+ }
325
+
326
+ const result = await promoService.releaseCode(code);
327
+ res.json(result);
328
+
329
+ } catch (error) {
330
+ console.error('[PromoRoutes] Error releasing code:', error.message);
331
+ res.status(500).json({ success: false, error: 'Server error releasing code' });
332
+ }
333
+ });
334
+
335
+ /**
336
+ * POST /promo/confirm-usage
337
+ * Confirm that a promo code was used (called AFTER transaction is confirmed on-chain)
338
+ * This is the only way to mark a promo as 'used' - prevents marking as used if user cancels
339
+ *
340
+ * Body: { code: "DUBS-XXXX-XXXX", walletAddress: "...", gameId: "...", signature: "..." }
341
+ * Response: { success: boolean, error?: string }
342
+ */
343
+ router.post('/confirm-usage', async (req, res) => {
344
+ try {
345
+ const { code, walletAddress, gameId, signature } = req.body;
346
+
347
+ if (!code) {
348
+ return res.status(400).json({ success: false, error: 'Code is required' });
349
+ }
350
+
351
+ if (!walletAddress) {
352
+ return res.status(400).json({ success: false, error: 'Wallet address is required' });
353
+ }
354
+
355
+ if (!gameId) {
356
+ return res.status(400).json({ success: false, error: 'Game ID is required' });
357
+ }
358
+
359
+ if (!signature) {
360
+ return res.status(400).json({ success: false, error: 'Transaction signature is required - promo can only be marked used after confirmed tx' });
361
+ }
362
+
363
+ console.log('[PromoRoutes] Confirming usage:', { code, walletAddress, gameId, signature: signature.slice(0, 20) + '...' });
364
+
365
+ // Verify the code belongs to this wallet and is reserved
366
+ const activePromo = await promoService.getActivePromoForUser(walletAddress);
367
+
368
+ if (!activePromo) {
369
+ return res.status(400).json({ success: false, error: 'No active promo found for this wallet' });
370
+ }
371
+
372
+ if (activePromo.code !== code.trim().toUpperCase()) {
373
+ return res.status(400).json({
374
+ success: false,
375
+ error: `Code mismatch. Expected: ${activePromo.code}, Got: ${code}`
376
+ });
377
+ }
378
+
379
+ // Mark as used
380
+ const result = await promoService.markCodeAsUsed(code, walletAddress, gameId);
381
+
382
+ if (result.success) {
383
+ console.log('[PromoRoutes] ✅ Promo confirmed as used:', code, 'for game:', gameId);
384
+ }
385
+
386
+ res.json(result);
387
+
388
+ } catch (error) {
389
+ console.error('[PromoRoutes] Error confirming usage:', error.message);
390
+ res.status(500).json({ success: false, error: 'Server error confirming promo usage' });
391
+ }
392
+ });
393
+
394
+ /**
395
+ * POST /promo/dismiss-banner
396
+ * Mark the promo banner as dismissed for a user
397
+ *
398
+ * Body: { walletAddress: "..." }
399
+ * Response: { success: boolean }
400
+ */
401
+ router.post('/dismiss-banner', async (req, res) => {
402
+ try {
403
+ const { walletAddress } = req.body;
404
+
405
+ if (!walletAddress) {
406
+ return res.status(400).json({ success: false, error: 'Wallet address is required' });
407
+ }
408
+
409
+ const result = await promoService.dismissBanner(walletAddress);
410
+ res.json(result);
411
+
412
+ } catch (error) {
413
+ console.error('[PromoRoutes] Error dismissing banner:', error.message);
414
+ res.status(500).json({ success: false, error: 'Server error dismissing banner' });
415
+ }
416
+ });
417
+
418
+ module.exports = router;