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.
- package/.claude/settings.local.json +280 -0
- package/CLAUDE.md +46 -0
- package/CONNECT4_PRODUCTION_DEPLOY.md +155 -0
- package/CURRENT_SESSION.md +171 -0
- package/CURRENT_SESSION_DRAW.md +516 -0
- package/MARCH_MADNESS_SURVIVOR.md +254 -0
- package/PANDA.md +166 -0
- package/Procfile +4 -0
- package/README.md +476 -0
- package/controllers/livescoresController.js +376 -0
- package/controllers/pickemController.js +554 -0
- package/controllers/survivorAdminController.js +887 -0
- package/controllers/survivorController.js +623 -0
- package/cron/oracleMonitor.js +77 -0
- package/cron/pickemOracleMonitor.js +73 -0
- package/data/jackpot-history.json +952 -0
- package/data/ncaaTeams.js +406 -0
- package/documentation/API_SECURITY_GUIDE.md +327 -0
- package/documentation/ARCADE_API.md +593 -0
- package/documentation/ARCADE_IMPLEMENTATION_SUMMARY.md +399 -0
- package/documentation/ARCADE_QUICKSTART.md +242 -0
- package/documentation/AUTOMATIC_MODE_ORACLE.md +321 -0
- package/documentation/BUG_FIX_COHORT_DATE_DISPLAY.md +171 -0
- package/documentation/CLAIM_MIGRATION_INSTRUCTIONS.md +52 -0
- package/documentation/CLAIM_STATUS_FIX.md +67 -0
- package/documentation/CLI_TOOL_GUIDE.md +372 -0
- package/documentation/COHORT_RETENTION_ANALYSIS.md +295 -0
- package/documentation/COHORT_RETENTION_IMPLEMENTATION_COMPLETE.md +461 -0
- package/documentation/COHORT_RETENTION_SUMMARY.md +204 -0
- package/documentation/COMPLETE_PROJECT_SUMMARY.md +490 -0
- package/documentation/DATABASE_QUERIES.md +269 -0
- package/documentation/DATABASE_RETENTION_POLICY.md +390 -0
- package/documentation/DATABASE_SETUP_GUIDE.md +361 -0
- package/documentation/DATABASE_SETUP_SUMMARY.md +247 -0
- package/documentation/DEMO_API_CURL_COMMANDS.md +656 -0
- package/documentation/DEPLOYMENT_SUMMARY.txt +100 -0
- package/documentation/DUPLICATE_NOTIFICATIONS_FIXED.md +201 -0
- package/documentation/EXCHANGE_RATES_INTEGRATION.md +371 -0
- package/documentation/FINAL_API_PROTECTION_TABLE.md +175 -0
- package/documentation/GAME_START_NOTIFICATIONS_DEPLOYMENT.md +256 -0
- package/documentation/GAME_START_NOTIFICATIONS_INTEGRATION.md +275 -0
- package/documentation/HEROKU_DEPLOYMENT.md +134 -0
- package/documentation/HEROKU_SCHEDULER_SETUP.md +271 -0
- package/documentation/JACKPOT_API.md +521 -0
- package/documentation/JACKPOT_DEPLOYMENT_GUIDE.md +362 -0
- package/documentation/JWT_IMPLEMENTATION_SUMMARY.md +373 -0
- package/documentation/JWT_QUICK_SETUP.md +268 -0
- package/documentation/JWT_TESTING_GUIDE.md +404 -0
- package/documentation/KEEPER_RECOVERY_GUIDE.md +381 -0
- package/documentation/KEEPER_SETUP.md +206 -0
- package/documentation/KEEPER_STATE_MACHINE.md +423 -0
- package/documentation/LATEST_PRODUCTION_SETUP.md +387 -0
- package/documentation/LOCAL_VOTING_TEST.md +279 -0
- package/documentation/ORACLE_FIXES_SUMMARY.md +188 -0
- package/documentation/ORACLE_POSTGRESQL_UPDATE.md +202 -0
- package/documentation/PAYMENT_DEPLOYMENT.md +209 -0
- package/documentation/PNL_TRACKING_SETUP.md +189 -0
- package/documentation/PREVENTING_LOCKUP_ERRORS.md +472 -0
- package/documentation/PRODUCTION_READY_SUMMARY.md +227 -0
- package/documentation/PUBLIC_VS_PRIVATE_ENDPOINTS.md +278 -0
- package/documentation/QUICK_AUTH_SETUP.md +99 -0
- package/documentation/QUICK_DEPLOY.md +224 -0
- package/documentation/QUICK_FIX.md +114 -0
- package/documentation/QUICK_START.md +152 -0
- package/documentation/REFEREE_MODE_GUIDE.md +392 -0
- package/documentation/RETENTION_CORE_ACTION_UPDATE.md +313 -0
- package/documentation/RETENTION_UPDATE_SUMMARY.md +108 -0
- package/documentation/RUN_MIGRATION_NOW.md +39 -0
- package/documentation/SCRIPTS_UPDATE_SUMMARY.md +251 -0
- package/documentation/SETUP_GUIDE.md +184 -0
- package/documentation/STATE_MACHINE_IMPLEMENTATION.md +250 -0
- package/documentation/TELEGRAM_NOTIFICATIONS_DIAGNOSIS.md +361 -0
- package/documentation/UNIFIED_ARCHITECTURE.md +231 -0
- package/documentation/VOTING_DEPLOYMENT_SUMMARY.md +392 -0
- package/documentation/WEBSOCKET_ARCHITECTURE.md +881 -0
- package/documentation/WHAT_WE_BUILT_TODAY.md +369 -0
- package/documentation/latest/LATEST_PRODUCTION_SETUP.md +865 -0
- package/ecosystem.config.js +65 -0
- package/env.template +125 -0
- package/middleware/apiKeyAuth.js +136 -0
- package/middleware/authenticate.js +214 -0
- package/middleware/developerUserAuth.js +76 -0
- package/middleware/socketAuth.js +69 -0
- package/package.json +49 -0
- package/postman/Dubs-API-v1-With-Voting.postman_collection.json +555 -0
- package/postman/Dubs-API-v1.postman_collection.json +205 -0
- package/postman/Dubs_Developer_API.postman_collection.json +662 -0
- package/postman/QUICKSTART.md +118 -0
- package/postman/QUICK_REFERENCE.md +246 -0
- package/postman/README.md +71 -0
- package/postman/VOTING_API_GUIDE.md +426 -0
- package/refactor/Animations.md +148 -0
- package/refactor/Chat.md +252 -0
- package/routes/actionsRoutes.js +699 -0
- package/routes/adminRoutes.js +370 -0
- package/routes/analyticsRoutes.js +1262 -0
- package/routes/arcadeRoutes.js +557 -0
- package/routes/authRoutes.js +2310 -0
- package/routes/avatarRoutes.js +85 -0
- package/routes/botRoutes.js +211 -0
- package/routes/chatRoutes.js +377 -0
- package/routes/cryptoPriceRoutes.js +105 -0
- package/routes/developerRoutes.js +4201 -0
- package/routes/deviceRoutes.js +214 -0
- package/routes/dmRoutes.js +167 -0
- package/routes/esportsRoutes.js +806 -0
- package/routes/exchangeRateRoutes.js +233 -0
- package/routes/gamesRoutes.js +3028 -0
- package/routes/jackpotRoutes.js +754 -0
- package/routes/keeperMonitoringRoutes.js +156 -0
- package/routes/keeperWebhookRoutes.js +466 -0
- package/routes/livescoresRoutes.js +31 -0
- package/routes/pickemAdminRoutes.js +199 -0
- package/routes/pickemRoutes.js +231 -0
- package/routes/playerStatsRoutes.js +147 -0
- package/routes/portfolioRoutes.js +217 -0
- package/routes/promoRoutes.js +418 -0
- package/routes/referralEarningsRoutes.js +392 -0
- package/routes/socialRoutes.js +459 -0
- package/routes/sportsRoutes.js +1271 -0
- package/routes/survivorAdminRoutes.js +345 -0
- package/routes/survivorRoutes.js +756 -0
- package/routes/uploadRoutes.js +256 -0
- package/routes/userProfileRoutes.js +244 -0
- package/routes/whatsNewRoutes.js +331 -0
- package/scripts/.claude/settings.local.json +15 -0
- package/scripts/README.md +170 -0
- package/scripts/RESTART_EVERYTHING.sh +104 -0
- package/scripts/add-claim-columns.sql +48 -0
- package/scripts/add-crypto-prices-cache.sql +27 -0
- package/scripts/add-exchange-rates-cache.sql +40 -0
- package/scripts/add-game-invite-column.sql +23 -0
- package/scripts/add-game-invite-notification.sql +33 -0
- package/scripts/add-game-invite-telegram-pref.sql +16 -0
- package/scripts/add-game-joined-notification.sql +16 -0
- package/scripts/add-game-joined-pref.js +40 -0
- package/scripts/add-game-joined-preference.sql +6 -0
- package/scripts/add-game-start-notifications.sql +41 -0
- package/scripts/add-notification-flags-to-games.sql +55 -0
- package/scripts/add-pending-game-dismissals.sql +19 -0
- package/scripts/add-preferred-currency.sql +34 -0
- package/scripts/add-winner-columns.js +61 -0
- package/scripts/add_mention_system.sql +53 -0
- package/scripts/add_payment_system.sql +96 -0
- package/scripts/add_sports_event_id_column.sql +22 -0
- package/scripts/analyze-cohort-data-heroku.js +276 -0
- package/scripts/analyze-cohort-data.js +295 -0
- package/scripts/analyze-prod-cohorts.sh +10 -0
- package/scripts/backfill-matchup-images.js +245 -0
- package/scripts/backfill-missing-signatures.js +175 -0
- package/scripts/backfill-referral-earnings.js +202 -0
- package/scripts/check-chat-schema.js +130 -0
- package/scripts/check-db.sh +14 -0
- package/scripts/check_oracle_in_game.js +54 -0
- package/scripts/cleanup-database.js +193 -0
- package/scripts/clear-notification-cache.js +85 -0
- package/scripts/convert-mnemonic.js +50 -0
- package/scripts/create-users-table.sql +44 -0
- package/scripts/debug-cohort-counts.js +248 -0
- package/scripts/debug-winner-calc.js +84 -0
- package/scripts/deploy-payment-system.sh +118 -0
- package/scripts/deploy-to-heroku.sh +63 -0
- package/scripts/diagnose-locked-round.js +143 -0
- package/scripts/dubs-cli.js +720 -0
- package/scripts/dump-account.js +65 -0
- package/scripts/find-vrf-offset.js +48 -0
- package/scripts/fix-chat-notifications-constraint.sql +122 -0
- package/scripts/fix-claim-columns.js +124 -0
- package/scripts/fix-constraint-now.js +44 -0
- package/scripts/fix-lock-timestamps.js +96 -0
- package/scripts/fix-locked-round.sh +126 -0
- package/scripts/fix-missing-badges.sql +91 -0
- package/scripts/fix-payment-notifications.sql +41 -0
- package/scripts/force-new-round.js +55 -0
- package/scripts/force-resolve-and-claim.js +278 -0
- package/scripts/important/README.md +115 -0
- package/scripts/important/authority-force-lock.js +197 -0
- package/scripts/important/authority-resolve-game.js +267 -0
- package/scripts/important/check-game-status.js +373 -0
- package/scripts/important/list-pending-games-by-version.js +270 -0
- package/scripts/important/reconcile-v1-v2-payouts.js +270 -0
- package/scripts/initialize-jackpot.js +111 -0
- package/scripts/jackpot/.claude/settings.local.json +10 -0
- package/scripts/jackpot/force-reset.js +84 -0
- package/scripts/jackpot/initialize-mainnet.js +100 -0
- package/scripts/jackpot/keeper.js +742 -0
- package/scripts/jackpot/status.js +107 -0
- package/scripts/jackpot/update-round-duration.js +143 -0
- package/scripts/keeper-bot.js +112 -0
- package/scripts/list-pending-games.js +131 -0
- package/scripts/migrate-chat-v2.js +127 -0
- package/scripts/migrate-chat-winners.js +84 -0
- package/scripts/migrate-chat.sh +17 -0
- package/scripts/migrate-game-invite.js +83 -0
- package/scripts/migrate-heroku-game-notifications.sh +159 -0
- package/scripts/migrations/001_analytics_tables.sql +422 -0
- package/scripts/migrations/002_add_matchup_image_url.sql +14 -0
- package/scripts/migrations/003_referral_earnings.sql +208 -0
- package/scripts/migrations/004_add_whats_new_notification_type.sql +62 -0
- package/scripts/migrations/005_add_connect4_your_turn_notification.sql +61 -0
- package/scripts/migrations/005_push_notifications.sql +55 -0
- package/scripts/migrations/006_add_draw_team_players.sql +28 -0
- package/scripts/migrations/006_add_game_cancelled_notification.sql +62 -0
- package/scripts/migrations/007_add_gif_url.sql +8 -0
- package/scripts/migrations/008_add_connect4_columns.sql +139 -0
- package/scripts/migrations/008_add_pool_tracking.sql +22 -0
- package/scripts/migrations/009_create_survivor_pool_tables.sql +174 -0
- package/scripts/migrations/010_add_survivor_pool_outcome.sql +28 -0
- package/scripts/migrations/011_create_developer_tables.sql +67 -0
- package/scripts/migrations/011_fix_keeper_tables.sql +85 -0
- package/scripts/migrations/012_create_developer_webhooks.sql +31 -0
- package/scripts/migrations/013_add_network_mode.sql +18 -0
- package/scripts/migrations/014_create_developer_app_users.sql +19 -0
- package/scripts/migrations/015_add_ui_config.sql +4 -0
- package/scripts/migrations/016_add_resolution_secret.sql +4 -0
- package/scripts/migrations/017_add_external_game_id.sql +3 -0
- package/scripts/migrations/018_create_pickem_tables.sql +115 -0
- package/scripts/migrations/019_expo_push_tokens.sql +19 -0
- package/scripts/migrations/create_whats_new_tables.sql +88 -0
- package/scripts/migrations/drop_live_games_tables.sql +34 -0
- package/scripts/open-jackpot-round.js +85 -0
- package/scripts/purge-all-data.sh +329 -0
- package/scripts/purge-all-data.sql +142 -0
- package/scripts/purge-heroku-data.sh +149 -0
- package/scripts/purge-heroku-data.sql +62 -0
- package/scripts/rebuild-heroku-database.sh +113 -0
- package/scripts/recover-funds.js +357 -0
- package/scripts/regenerate-epl-images.js +278 -0
- package/scripts/resize-s3-matchup-images.js +374 -0
- package/scripts/resolve-direct.js +88 -0
- package/scripts/resolve-mock-game.js +124 -0
- package/scripts/resolve-pickem-game.js +55 -0
- package/scripts/resolve-round-manual.js +83 -0
- package/scripts/resolve-stuck-game.js +382 -0
- package/scripts/resolve-stuck-round.js +42 -0
- package/scripts/run-connect4-migration.sh +16 -0
- package/scripts/run-mention-migration.sh +32 -0
- package/scripts/run-payment-migration.sh +51 -0
- package/scripts/run-preferred-currency-migration.sh +31 -0
- package/scripts/run-referral-earnings-migration.sh +32 -0
- package/scripts/run-survivor-outcome-migration.sh +16 -0
- package/scripts/seed-test-users.js +346 -0
- package/scripts/setup-auth-tables.js +78 -0
- package/scripts/setup-complete-database.sql +992 -0
- package/scripts/setup-database-fresh.sh +359 -0
- package/scripts/setup-heroku-keeper.sh +48 -0
- package/scripts/setup-keeper-database.js +83 -0
- package/scripts/setup-keeper-state-db.sql +110 -0
- package/scripts/setup-oracle.sh +39 -0
- package/scripts/setup-pnl-tracking.js +111 -0
- package/scripts/start-devnet.sh +14 -0
- package/scripts/test-arcade-devnet.sh +160 -0
- package/scripts/test-arcade-match.sh +109 -0
- package/scripts/test-automatic-mode.sh +239 -0
- package/scripts/test-connect4-cancel-claim.js +370 -0
- package/scripts/test-connect4-e2e.js +369 -0
- package/scripts/test-connect4-resolve.js +369 -0
- package/scripts/test-game-state-endpoint.js +136 -0
- package/scripts/test-invite-notification.js +86 -0
- package/scripts/test-jackpot-api.sh +71 -0
- package/scripts/test-poll-confirmation.js +267 -0
- package/scripts/test-resolve-game.js +271 -0
- package/scripts/test-resolve-signature.js +223 -0
- package/scripts/test-signature-preservation.js +124 -0
- package/scripts/test-state-machine.js +291 -0
- package/scripts/test-webhook-receiver.js +60 -0
- package/scripts/update-notification-constraint.js +52 -0
- package/scripts/verify-account-layout.js +145 -0
- package/scripts/verify-winner-algorithm.js +278 -0
- package/server.js +5259 -0
- package/services/arcadeMatchService.js +763 -0
- package/services/automaticGameOracle.js +1596 -0
- package/services/chatService.js +1612 -0
- package/services/connect4GameService.js +1049 -0
- package/services/connect4NotificationService.js +374 -0
- package/services/cryptoPriceService.js +223 -0
- package/services/customGameResolver.js +260 -0
- package/services/db.js +79 -0
- package/services/directMessageService.js +389 -0
- package/services/discordNotifications.js +160 -0
- package/services/exchangeRateService.js +289 -0
- package/services/expoPushService.js +314 -0
- package/services/gamesCacheService.js +539 -0
- package/services/jackpotHistory.js +331 -0
- package/services/jackpotService.js +856 -0
- package/services/keeperStateService.js +355 -0
- package/services/matchupImageService.js +591 -0
- package/services/notificationCacheService.js +407 -0
- package/services/pickemOracle.js +440 -0
- package/services/playerStatsService.js +389 -0
- package/services/portfolioService.js +555 -0
- package/services/promoService.js +757 -0
- package/services/promoTreasuryService.js +239 -0
- package/services/pushNotifications.js +353 -0
- package/services/redisService.js +422 -0
- package/services/referralEarningsService.js +728 -0
- package/services/s3Service.js +396 -0
- package/services/socialService.js +1202 -0
- package/services/survivorOracle.js +469 -0
- package/services/survivorSimulator.js +475 -0
- package/services/telegramNotifications.js +461 -0
- package/services/userProfileStatsService.js +1185 -0
- package/services/whatsNewService.js +388 -0
- package/utils/urlHelper.js +95 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pick'em Admin Routes - Simulation endpoints for testing fight cards
|
|
3
|
+
* Requires admin wallet in x-wallet-address header
|
|
4
|
+
* Emits Socket.IO events so connected clients react in real-time
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const express = require('express');
|
|
8
|
+
const router = express.Router();
|
|
9
|
+
const { pool } = require('../services/db');
|
|
10
|
+
const pickemController = require('../controllers/pickemController');
|
|
11
|
+
const expoPushService = require('../services/expoPushService');
|
|
12
|
+
|
|
13
|
+
const ADMIN_WALLET = 'Hvv1ctqHLR5wonuuRguefS6EpGUe7tFRBX2YWHGr3mes';
|
|
14
|
+
|
|
15
|
+
let io = null;
|
|
16
|
+
router.setSocketIO = (ioInstance) => {
|
|
17
|
+
io = ioInstance;
|
|
18
|
+
console.log('[PickemAdmin] Socket.IO injected');
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function emit(event, data) {
|
|
22
|
+
if (io) io.emit(event, data);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function requireAdmin(req, res, next) {
|
|
26
|
+
const walletAddress = req.headers['x-wallet-address'] || req.query.wallet;
|
|
27
|
+
if (!walletAddress || walletAddress !== ADMIN_WALLET) {
|
|
28
|
+
return res.status(403).json({ success: false, error: 'Admin access required' });
|
|
29
|
+
}
|
|
30
|
+
req.adminWallet = walletAddress;
|
|
31
|
+
next();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
router.use(requireAdmin);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* POST /api/pickem/admin/fights/:fightId/update
|
|
38
|
+
* Update a fight's status, winner, and method. Recomputes scores if fight is resolved.
|
|
39
|
+
*/
|
|
40
|
+
router.post('/fights/:fightId/update', async (req, res) => {
|
|
41
|
+
try {
|
|
42
|
+
const fightId = parseInt(req.params.fightId);
|
|
43
|
+
const { status, winner, method } = req.body;
|
|
44
|
+
|
|
45
|
+
if (!status) {
|
|
46
|
+
return res.status(400).json({ success: false, error: 'status is required' });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
console.log(`[PickemAdmin] Updating fight ${fightId}: status=${status}, winner=${winner || 'none'}, method=${method || 'none'}`);
|
|
50
|
+
|
|
51
|
+
const fight = await pickemController.updateFightResult(fightId, {
|
|
52
|
+
winner: winner || null,
|
|
53
|
+
method: method || null,
|
|
54
|
+
status,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (!fight) {
|
|
58
|
+
return res.status(404).json({ success: false, error: 'Fight not found' });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Emit fight status change
|
|
62
|
+
if (status === 'live') {
|
|
63
|
+
emit('pickem:fight_live', { poolId: fight.poolId, fight });
|
|
64
|
+
expoPushService.sendFightLiveNotifications(fightId, fight)
|
|
65
|
+
.catch(err => console.error('[PickemAdmin] Push error (fight live):', err.message));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let scores = null;
|
|
69
|
+
if (status === 'final' && winner) {
|
|
70
|
+
scores = await pickemController.computeScores(fight.poolId);
|
|
71
|
+
console.log(`[PickemAdmin] Scores recomputed for pool ${fight.poolId}: ${scores.entries.length} entries, max=${scores.maxScore}`);
|
|
72
|
+
emit('pickem:fight_resolved', { poolId: fight.poolId, fight, scores });
|
|
73
|
+
expoPushService.sendFightResolvedNotifications(fightId, fight)
|
|
74
|
+
.catch(err => console.error('[PickemAdmin] Push error (fight resolved):', err.message));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Always emit a generic update so clients know to refetch
|
|
78
|
+
emit('pickem:pool_updated', { poolId: fight.poolId });
|
|
79
|
+
|
|
80
|
+
res.json({ success: true, fight, scores });
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error('[PickemAdmin] Error updating fight:', error);
|
|
83
|
+
res.status(500).json({ success: false, error: error.message });
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* POST /api/pickem/admin/pools/:poolId/reset-fights
|
|
89
|
+
* Reset all fights to scheduled, clear winners/scores for re-testing
|
|
90
|
+
*/
|
|
91
|
+
router.post('/pools/:poolId/reset-fights', async (req, res) => {
|
|
92
|
+
try {
|
|
93
|
+
const poolId = parseInt(req.params.poolId);
|
|
94
|
+
const { resetPoolStatus } = req.body || {};
|
|
95
|
+
|
|
96
|
+
console.log(`[PickemAdmin] Resetting fights for pool ${poolId}`);
|
|
97
|
+
|
|
98
|
+
const fightsResult = await pool.query(
|
|
99
|
+
`UPDATE pickem_fights
|
|
100
|
+
SET status = 'scheduled', winner = NULL, method = NULL, updated_at = NOW()
|
|
101
|
+
WHERE pool_id = $1`,
|
|
102
|
+
[poolId]
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
await pool.query(
|
|
106
|
+
`UPDATE pickem_picks pp
|
|
107
|
+
SET is_correct = NULL, updated_at = NOW()
|
|
108
|
+
FROM pickem_entries pe
|
|
109
|
+
WHERE pp.entry_id = pe.id AND pe.pool_id = $1`,
|
|
110
|
+
[poolId]
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
await pool.query(
|
|
114
|
+
`UPDATE pickem_entries
|
|
115
|
+
SET score = 0, rank = NULL, updated_at = NOW()
|
|
116
|
+
WHERE pool_id = $1`,
|
|
117
|
+
[poolId]
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
if (resetPoolStatus) {
|
|
121
|
+
await pickemController.updatePool(poolId, { status: 'locked' });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
console.log(`[PickemAdmin] Reset ${fightsResult.rowCount} fights for pool ${poolId}`);
|
|
125
|
+
|
|
126
|
+
emit('pickem:pool_reset', { poolId });
|
|
127
|
+
emit('pickem:pool_updated', { poolId });
|
|
128
|
+
|
|
129
|
+
res.json({
|
|
130
|
+
success: true,
|
|
131
|
+
fightsReset: fightsResult.rowCount,
|
|
132
|
+
poolStatusReset: !!resetPoolStatus,
|
|
133
|
+
});
|
|
134
|
+
} catch (error) {
|
|
135
|
+
console.error('[PickemAdmin] Error resetting fights:', error);
|
|
136
|
+
res.status(500).json({ success: false, error: error.message });
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* POST /api/pickem/admin/pools/:poolId/status
|
|
142
|
+
* Change pool status
|
|
143
|
+
*/
|
|
144
|
+
router.post('/pools/:poolId/status', async (req, res) => {
|
|
145
|
+
try {
|
|
146
|
+
const poolId = parseInt(req.params.poolId);
|
|
147
|
+
const { status } = req.body;
|
|
148
|
+
|
|
149
|
+
if (!status) {
|
|
150
|
+
return res.status(400).json({ success: false, error: 'status is required' });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
console.log(`[PickemAdmin] Setting pool ${poolId} status to ${status}`);
|
|
154
|
+
|
|
155
|
+
const updatedPool = await pickemController.updatePool(poolId, { status });
|
|
156
|
+
|
|
157
|
+
if (status === 'locked') emit('pickem:pool_locked', { poolId });
|
|
158
|
+
emit('pickem:pool_updated', { poolId });
|
|
159
|
+
|
|
160
|
+
res.json({ success: true, pool: updatedPool });
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.error('[PickemAdmin] Error updating pool status:', error);
|
|
163
|
+
res.status(500).json({ success: false, error: error.message });
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* POST /api/pickem/admin/pools/:poolId/resolve
|
|
169
|
+
* Trigger full pool resolution: compute final scores + set complete
|
|
170
|
+
*/
|
|
171
|
+
router.post('/pools/:poolId/resolve', async (req, res) => {
|
|
172
|
+
try {
|
|
173
|
+
const poolId = parseInt(req.params.poolId);
|
|
174
|
+
|
|
175
|
+
console.log(`[PickemAdmin] Resolving pool ${poolId}`);
|
|
176
|
+
|
|
177
|
+
await pickemController.updatePool(poolId, { status: 'resolving' });
|
|
178
|
+
const scores = await pickemController.computeScores(poolId);
|
|
179
|
+
const updatedPool = await pickemController.updatePool(poolId, { status: 'complete' });
|
|
180
|
+
|
|
181
|
+
console.log(`[PickemAdmin] Pool ${poolId} resolved: ${scores.winnerCount} winner(s) with score ${scores.maxScore}`);
|
|
182
|
+
|
|
183
|
+
emit('pickem:pool_resolved', { poolId, scores });
|
|
184
|
+
emit('pickem:pool_updated', { poolId });
|
|
185
|
+
expoPushService.sendPoolResolvedNotifications(poolId, scores)
|
|
186
|
+
.catch(err => console.error('[PickemAdmin] Push error (pool resolved):', err.message));
|
|
187
|
+
|
|
188
|
+
res.json({
|
|
189
|
+
success: true,
|
|
190
|
+
pool: updatedPool,
|
|
191
|
+
scores,
|
|
192
|
+
});
|
|
193
|
+
} catch (error) {
|
|
194
|
+
console.error('[PickemAdmin] Error resolving pool:', error);
|
|
195
|
+
res.status(500).json({ success: false, error: error.message });
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
module.exports = router;
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pick'em Routes
|
|
3
|
+
* UFC Pick'em pool endpoints
|
|
4
|
+
* Pattern mirrors survivorRoutes.js
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const express = require('express');
|
|
8
|
+
const router = express.Router();
|
|
9
|
+
const pickemController = require('../controllers/pickemController');
|
|
10
|
+
const { authenticate } = require('../middleware/authenticate');
|
|
11
|
+
|
|
12
|
+
let io = null;
|
|
13
|
+
router.setSocketIO = (ioInstance) => {
|
|
14
|
+
io = ioInstance;
|
|
15
|
+
console.log('[Pickem] Socket.IO injected');
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// ========== POOL ENDPOINTS ==========
|
|
19
|
+
|
|
20
|
+
// Create a new pick'em pool (admin)
|
|
21
|
+
router.post('/pools', async (req, res) => {
|
|
22
|
+
try {
|
|
23
|
+
const { name, espnEventId, eventDate, lockTime, solanaGameId, solanaGameAddress } = req.body;
|
|
24
|
+
const createdBy = req.body.createdBy || req.headers['x-wallet-address'] || null;
|
|
25
|
+
|
|
26
|
+
if (!name || !eventDate || !lockTime) {
|
|
27
|
+
return res.status(400).json({ success: false, error: 'name, eventDate, and lockTime are required' });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const pool = await pickemController.createPool({
|
|
31
|
+
name, espnEventId, eventDate, lockTime, solanaGameId, solanaGameAddress, createdBy,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
res.json({ success: true, pool });
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error('[Pickem] Error creating pool:', error.message);
|
|
37
|
+
res.status(500).json({ success: false, error: error.message });
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// List pools
|
|
42
|
+
router.get('/pools', async (req, res) => {
|
|
43
|
+
try {
|
|
44
|
+
const { status } = req.query;
|
|
45
|
+
const pools = await pickemController.getPools({ status: status || undefined });
|
|
46
|
+
res.json({ success: true, pools });
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error('[Pickem] Error listing pools:', error.message);
|
|
49
|
+
res.status(500).json({ success: false, error: error.message });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Get pool detail with fights and stats
|
|
54
|
+
router.get('/pools/:id', async (req, res) => {
|
|
55
|
+
try {
|
|
56
|
+
const poolId = parseInt(req.params.id);
|
|
57
|
+
const pool = await pickemController.getPoolById(poolId);
|
|
58
|
+
if (!pool) return res.status(404).json({ success: false, error: 'Pool not found' });
|
|
59
|
+
|
|
60
|
+
const fights = await pickemController.getFights(poolId);
|
|
61
|
+
const stats = await pickemController.getPoolStats(poolId);
|
|
62
|
+
|
|
63
|
+
res.json({ success: true, pool, fights, stats });
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.error('[Pickem] Error getting pool:', error.message);
|
|
66
|
+
res.status(500).json({ success: false, error: error.message });
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Update a pool (authenticated — set solana_game_id/address after on-chain creation)
|
|
71
|
+
router.patch('/pools/:id', authenticate, async (req, res) => {
|
|
72
|
+
try {
|
|
73
|
+
const poolId = parseInt(req.params.id);
|
|
74
|
+
const pool = await pickemController.updatePool(poolId, req.body);
|
|
75
|
+
if (!pool) return res.status(404).json({ success: false, error: 'Pool not found' });
|
|
76
|
+
res.json({ success: true, pool });
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error('[Pickem] Error updating pool:', error.message);
|
|
79
|
+
res.status(500).json({ success: false, error: error.message });
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// ========== FIGHT ENDPOINTS ==========
|
|
84
|
+
|
|
85
|
+
// Get fights for a pool
|
|
86
|
+
router.get('/pools/:id/fights', async (req, res) => {
|
|
87
|
+
try {
|
|
88
|
+
const poolId = parseInt(req.params.id);
|
|
89
|
+
const fights = await pickemController.getFights(poolId);
|
|
90
|
+
res.json({ success: true, count: fights.length, fights });
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.error('[Pickem] Error getting fights:', error.message);
|
|
93
|
+
res.status(500).json({ success: false, error: error.message });
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Import fights for a pool (admin)
|
|
98
|
+
router.post('/pools/:id/fights', async (req, res) => {
|
|
99
|
+
try {
|
|
100
|
+
const poolId = parseInt(req.params.id);
|
|
101
|
+
const { fights } = req.body;
|
|
102
|
+
|
|
103
|
+
if (!fights || !Array.isArray(fights) || fights.length === 0) {
|
|
104
|
+
return res.status(400).json({ success: false, error: 'fights array is required' });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const imported = await pickemController.importFights(poolId, fights);
|
|
108
|
+
res.json({ success: true, count: imported.length, fights: imported });
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error('[Pickem] Error importing fights:', error.message);
|
|
111
|
+
res.status(500).json({ success: false, error: error.message });
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// ========== ENTRY ENDPOINTS ==========
|
|
116
|
+
|
|
117
|
+
// Enter a pool (authenticated — after on-chain join)
|
|
118
|
+
router.post('/pools/:id/enter', authenticate, async (req, res) => {
|
|
119
|
+
try {
|
|
120
|
+
const poolId = parseInt(req.params.id);
|
|
121
|
+
const { txSignature } = req.body;
|
|
122
|
+
|
|
123
|
+
const entry = await pickemController.joinPool({
|
|
124
|
+
poolId,
|
|
125
|
+
userId: req.user.userId,
|
|
126
|
+
walletAddress: req.user.walletAddress,
|
|
127
|
+
txSignature,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
if (io) {
|
|
131
|
+
io.emit('pickem:entry_joined', { poolId, entry });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
res.json({ success: true, entry });
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.error('[Pickem] Error joining pool:', error.message);
|
|
137
|
+
const status = error.message.includes('not found') ? 404
|
|
138
|
+
: error.message.includes('already') || error.message.includes('duplicate') ? 409
|
|
139
|
+
: 400;
|
|
140
|
+
res.status(status).json({ success: false, error: error.message });
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// ========== PICK ENDPOINTS ==========
|
|
145
|
+
|
|
146
|
+
// Submit all picks at once (authenticated)
|
|
147
|
+
router.post('/pools/:id/picks', authenticate, async (req, res) => {
|
|
148
|
+
try {
|
|
149
|
+
const poolId = parseInt(req.params.id);
|
|
150
|
+
const { picks } = req.body;
|
|
151
|
+
|
|
152
|
+
if (!picks || !Array.isArray(picks)) {
|
|
153
|
+
return res.status(400).json({ success: false, error: 'picks array is required' });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const submitted = await pickemController.submitPicks({
|
|
157
|
+
poolId,
|
|
158
|
+
userId: req.user.userId,
|
|
159
|
+
picks,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (io) {
|
|
163
|
+
io.emit('pickem:picks_submitted', { poolId, userId: req.user.userId });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
res.json({ success: true, picks: submitted });
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.error('[Pickem] Error submitting picks:', error.message);
|
|
169
|
+
const status = error.message.includes('not found') ? 404
|
|
170
|
+
: error.message.includes('Lock time') ? 403
|
|
171
|
+
: 400;
|
|
172
|
+
res.status(status).json({ success: false, error: error.message });
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Get user's picks (authenticated)
|
|
177
|
+
router.get('/pools/:id/my-picks', authenticate, async (req, res) => {
|
|
178
|
+
try {
|
|
179
|
+
const poolId = parseInt(req.params.id);
|
|
180
|
+
const entry = await pickemController.getUserEntry(poolId, req.user.userId);
|
|
181
|
+
|
|
182
|
+
if (!entry) {
|
|
183
|
+
return res.status(404).json({ success: false, error: 'Not entered in this pool' });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
res.json({ success: true, entry });
|
|
187
|
+
} catch (error) {
|
|
188
|
+
console.error('[Pickem] Error getting picks:', error.message);
|
|
189
|
+
res.status(500).json({ success: false, error: error.message });
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// ========== LEADERBOARD / RESULTS ==========
|
|
194
|
+
|
|
195
|
+
// Leaderboard (public)
|
|
196
|
+
router.get('/pools/:id/leaderboard', async (req, res) => {
|
|
197
|
+
try {
|
|
198
|
+
const poolId = parseInt(req.params.id);
|
|
199
|
+
const leaderboard = await pickemController.getLeaderboard(poolId);
|
|
200
|
+
const stats = await pickemController.getPoolStats(poolId);
|
|
201
|
+
res.json({ success: true, totalEntries: leaderboard.length, leaderboard, stats });
|
|
202
|
+
} catch (error) {
|
|
203
|
+
console.error('[Pickem] Error getting leaderboard:', error.message);
|
|
204
|
+
res.status(500).json({ success: false, error: error.message });
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// Final results (public, pool must be complete)
|
|
209
|
+
router.get('/pools/:id/results', async (req, res) => {
|
|
210
|
+
try {
|
|
211
|
+
const poolId = parseInt(req.params.id);
|
|
212
|
+
const pool = await pickemController.getPoolById(poolId);
|
|
213
|
+
if (!pool) return res.status(404).json({ success: false, error: 'Pool not found' });
|
|
214
|
+
if (pool.status !== 'complete') {
|
|
215
|
+
return res.status(400).json({ success: false, error: `Pool is ${pool.status}, results not available yet` });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const fights = await pickemController.getFights(poolId);
|
|
219
|
+
const leaderboard = await pickemController.getLeaderboard(poolId);
|
|
220
|
+
const winners = await pickemController.getWinners(poolId);
|
|
221
|
+
const payouts = await pickemController.getPayouts(poolId);
|
|
222
|
+
const stats = await pickemController.getPoolStats(poolId);
|
|
223
|
+
|
|
224
|
+
res.json({ success: true, pool, fights, leaderboard, winners, payouts, stats });
|
|
225
|
+
} catch (error) {
|
|
226
|
+
console.error('[Pickem] Error getting results:', error.message);
|
|
227
|
+
res.status(500).json({ success: false, error: error.message });
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
module.exports = router;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 📊 Player Stats API Routes
|
|
3
|
+
*
|
|
4
|
+
* Endpoints for player PNL, history, and leaderboards
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const express = require('express');
|
|
8
|
+
const router = express.Router();
|
|
9
|
+
const { authenticate } = require('../middleware/authenticate');
|
|
10
|
+
|
|
11
|
+
module.exports = (playerStatsService) => {
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* GET /stats/player/:walletAddress
|
|
15
|
+
* Get player statistics (PUBLIC - basic stats for profile cards)
|
|
16
|
+
*/
|
|
17
|
+
router.get('/player/:walletAddress', async (req, res) => {
|
|
18
|
+
try {
|
|
19
|
+
const { walletAddress } = req.params;
|
|
20
|
+
|
|
21
|
+
const stats = await playerStatsService.getPlayerStats(walletAddress);
|
|
22
|
+
|
|
23
|
+
if (!stats) {
|
|
24
|
+
return res.status(404).json({
|
|
25
|
+
success: false,
|
|
26
|
+
error: 'Player not found'
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
res.json({
|
|
31
|
+
success: true,
|
|
32
|
+
stats
|
|
33
|
+
});
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error('Error fetching player stats:', error);
|
|
36
|
+
res.status(500).json({
|
|
37
|
+
success: false,
|
|
38
|
+
error: error.message
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* GET /stats/player/:walletAddress/history
|
|
45
|
+
* Get player history for chart (SECURED - detailed betting history is sensitive)
|
|
46
|
+
*/
|
|
47
|
+
router.get('/player/:walletAddress/history', authenticate, async (req, res) => {
|
|
48
|
+
try {
|
|
49
|
+
const { walletAddress } = req.params;
|
|
50
|
+
const limit = parseInt(req.query.limit) || 100;
|
|
51
|
+
|
|
52
|
+
const history = await playerStatsService.getPlayerHistory(walletAddress, limit);
|
|
53
|
+
|
|
54
|
+
res.json({
|
|
55
|
+
success: true,
|
|
56
|
+
history,
|
|
57
|
+
count: history.length
|
|
58
|
+
});
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error('Error fetching player history:', error);
|
|
61
|
+
res.status(500).json({
|
|
62
|
+
success: false,
|
|
63
|
+
error: error.message
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* GET /stats/leaderboard
|
|
70
|
+
* Get top players by PNL
|
|
71
|
+
*/
|
|
72
|
+
router.get('/leaderboard', async (req, res) => {
|
|
73
|
+
try {
|
|
74
|
+
const limit = parseInt(req.query.limit) || 10;
|
|
75
|
+
|
|
76
|
+
const leaderboard = await playerStatsService.getLeaderboard(limit);
|
|
77
|
+
|
|
78
|
+
res.json({
|
|
79
|
+
success: true,
|
|
80
|
+
leaderboard,
|
|
81
|
+
count: leaderboard.length
|
|
82
|
+
});
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error('Error fetching leaderboard:', error);
|
|
85
|
+
res.status(500).json({
|
|
86
|
+
success: false,
|
|
87
|
+
error: error.message
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* GET /stats/lucky
|
|
94
|
+
* Get the luckiest player (highest current winning streak)
|
|
95
|
+
*/
|
|
96
|
+
router.get('/lucky', async (req, res) => {
|
|
97
|
+
try {
|
|
98
|
+
const luckyPlayer = await playerStatsService.getLuckiestPlayer();
|
|
99
|
+
|
|
100
|
+
if (!luckyPlayer) {
|
|
101
|
+
return res.json({
|
|
102
|
+
success: true,
|
|
103
|
+
lucky: null,
|
|
104
|
+
message: 'No active winning streaks'
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
res.json({
|
|
109
|
+
success: true,
|
|
110
|
+
lucky: luckyPlayer
|
|
111
|
+
});
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.error('Error fetching luckiest player:', error);
|
|
114
|
+
res.status(500).json({
|
|
115
|
+
success: false,
|
|
116
|
+
error: error.message
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* GET /stats/streaks
|
|
123
|
+
* Get streak leaderboard (top players by current winning streak)
|
|
124
|
+
*/
|
|
125
|
+
router.get('/streaks', async (req, res) => {
|
|
126
|
+
try {
|
|
127
|
+
const limit = parseInt(req.query.limit) || 10;
|
|
128
|
+
|
|
129
|
+
const streakLeaderboard = await playerStatsService.getStreakLeaderboard(limit);
|
|
130
|
+
|
|
131
|
+
res.json({
|
|
132
|
+
success: true,
|
|
133
|
+
streaks: streakLeaderboard,
|
|
134
|
+
count: streakLeaderboard.length
|
|
135
|
+
});
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.error('Error fetching streak leaderboard:', error);
|
|
138
|
+
res.status(500).json({
|
|
139
|
+
success: false,
|
|
140
|
+
error: error.message
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
return router;
|
|
146
|
+
};
|
|
147
|
+
|