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,469 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Survivor Pool Oracle Service
|
|
3
|
+
* Monitors tournament games and resolves rounds based on ESPN results
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const axios = require('axios');
|
|
7
|
+
const { pool } = require('./db');
|
|
8
|
+
|
|
9
|
+
// ESPN NCAAB scoreboard URL
|
|
10
|
+
const ESPN_NCAAB_URL = 'https://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/scoreboard';
|
|
11
|
+
|
|
12
|
+
// Round names for logging
|
|
13
|
+
const ROUND_NAMES = {
|
|
14
|
+
1: 'First Four',
|
|
15
|
+
2: 'Round of 64',
|
|
16
|
+
3: 'Round of 32',
|
|
17
|
+
4: 'Sweet 16',
|
|
18
|
+
5: 'Elite 8',
|
|
19
|
+
6: 'Final Four',
|
|
20
|
+
7: 'Championship'
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
class SurvivorOracle {
|
|
24
|
+
constructor(config = {}) {
|
|
25
|
+
this.checkIntervalMs = config.checkIntervalMs || 5 * 60 * 1000; // 5 minutes default
|
|
26
|
+
this.isRunning = false;
|
|
27
|
+
this.intervalId = null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Start the oracle monitor
|
|
32
|
+
*/
|
|
33
|
+
start() {
|
|
34
|
+
if (this.isRunning) {
|
|
35
|
+
console.log('[SurvivorOracle] Already running');
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
console.log('[SurvivorOracle] Starting...');
|
|
40
|
+
console.log(` Check interval: ${this.checkIntervalMs / 1000}s`);
|
|
41
|
+
|
|
42
|
+
this.isRunning = true;
|
|
43
|
+
|
|
44
|
+
// Run immediately
|
|
45
|
+
this.checkActivePools();
|
|
46
|
+
|
|
47
|
+
// Then run on interval
|
|
48
|
+
this.intervalId = setInterval(() => {
|
|
49
|
+
this.checkActivePools();
|
|
50
|
+
}, this.checkIntervalMs);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Stop the oracle
|
|
55
|
+
*/
|
|
56
|
+
stop() {
|
|
57
|
+
if (this.intervalId) {
|
|
58
|
+
clearInterval(this.intervalId);
|
|
59
|
+
this.intervalId = null;
|
|
60
|
+
}
|
|
61
|
+
this.isRunning = false;
|
|
62
|
+
console.log('[SurvivorOracle] Stopped');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Check all active survivor pools
|
|
67
|
+
*/
|
|
68
|
+
async checkActivePools() {
|
|
69
|
+
try {
|
|
70
|
+
console.log(`\n[SurvivorOracle] [${new Date().toISOString()}] Checking active pools...`);
|
|
71
|
+
|
|
72
|
+
// Get all active pools
|
|
73
|
+
const result = await pool.query(
|
|
74
|
+
"SELECT * FROM survivor_pools WHERE status = 'active'"
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const activePools = result.rows;
|
|
78
|
+
console.log(`[SurvivorOracle] Found ${activePools.length} active pool(s)`);
|
|
79
|
+
|
|
80
|
+
for (const survivorPool of activePools) {
|
|
81
|
+
await this.processPool(survivorPool);
|
|
82
|
+
}
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error('[SurvivorOracle] Error checking pools:', error.message);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Process a single pool
|
|
90
|
+
*/
|
|
91
|
+
async processPool(survivorPool) {
|
|
92
|
+
try {
|
|
93
|
+
console.log(`[SurvivorOracle] Processing pool ${survivorPool.id}: ${survivorPool.name}`);
|
|
94
|
+
console.log(` Current round: ${survivorPool.current_round} (${ROUND_NAMES[survivorPool.current_round]})`);
|
|
95
|
+
|
|
96
|
+
// Check if round deadline has passed
|
|
97
|
+
if (survivorPool.round_deadline && new Date() < new Date(survivorPool.round_deadline)) {
|
|
98
|
+
console.log(` Deadline not yet passed, skipping`);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Get unresolved games for current round
|
|
103
|
+
const gamesResult = await pool.query(
|
|
104
|
+
`SELECT * FROM survivor_tournament_games
|
|
105
|
+
WHERE pool_id = $1 AND round = $2 AND status != 'final'`,
|
|
106
|
+
[survivorPool.id, survivorPool.current_round]
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const unresolvedGames = gamesResult.rows;
|
|
110
|
+
console.log(` Unresolved games: ${unresolvedGames.length}`);
|
|
111
|
+
|
|
112
|
+
if (unresolvedGames.length > 0) {
|
|
113
|
+
// Fetch ESPN scores and update games
|
|
114
|
+
await this.updateGameScores(survivorPool.id, unresolvedGames);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Check if all games in current round are final
|
|
118
|
+
const allGamesResult = await pool.query(
|
|
119
|
+
`SELECT
|
|
120
|
+
COUNT(*) as total,
|
|
121
|
+
COUNT(*) FILTER (WHERE status = 'final') as final_count
|
|
122
|
+
FROM survivor_tournament_games
|
|
123
|
+
WHERE pool_id = $1 AND round = $2`,
|
|
124
|
+
[survivorPool.id, survivorPool.current_round]
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const { total, final_count } = allGamesResult.rows[0];
|
|
128
|
+
console.log(` Games status: ${final_count}/${total} final`);
|
|
129
|
+
|
|
130
|
+
if (parseInt(total) > 0 && parseInt(final_count) === parseInt(total)) {
|
|
131
|
+
// All games final - resolve the round
|
|
132
|
+
console.log(` All games final - resolving round`);
|
|
133
|
+
await this.resolveCurrentRound(survivorPool.id);
|
|
134
|
+
}
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.error(`[SurvivorOracle] Error processing pool ${survivorPool.id}:`, error.message);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Fetch ESPN scores and update tournament games
|
|
142
|
+
*/
|
|
143
|
+
async updateGameScores(poolId, games) {
|
|
144
|
+
try {
|
|
145
|
+
console.log(`[SurvivorOracle] Fetching ESPN scores...`);
|
|
146
|
+
|
|
147
|
+
const { data } = await axios.get(ESPN_NCAAB_URL);
|
|
148
|
+
|
|
149
|
+
if (!data.events) {
|
|
150
|
+
console.log('[SurvivorOracle] No events in ESPN response');
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Create a map of ESPN game IDs to results
|
|
155
|
+
const espnResults = new Map();
|
|
156
|
+
|
|
157
|
+
for (const event of data.events) {
|
|
158
|
+
const competition = event.competitions?.[0];
|
|
159
|
+
if (!competition) continue;
|
|
160
|
+
|
|
161
|
+
const gameId = event.id;
|
|
162
|
+
const status = event.status?.type?.description;
|
|
163
|
+
const competitors = competition.competitors || [];
|
|
164
|
+
|
|
165
|
+
if (competitors.length !== 2) continue;
|
|
166
|
+
|
|
167
|
+
const team1 = competitors.find(c => c.homeAway === 'home') || competitors[0];
|
|
168
|
+
const team2 = competitors.find(c => c.homeAway === 'away') || competitors[1];
|
|
169
|
+
|
|
170
|
+
espnResults.set(gameId, {
|
|
171
|
+
status,
|
|
172
|
+
isFinal: status === 'Final' || event.status?.type?.completed === true,
|
|
173
|
+
team1Score: parseInt(team1.score) || 0,
|
|
174
|
+
team2Score: parseInt(team2.score) || 0,
|
|
175
|
+
team1Id: team1.team?.id,
|
|
176
|
+
team2Id: team2.team?.id,
|
|
177
|
+
winnerId: team1.winner ? team1.team?.id : (team2.winner ? team2.team?.id : null)
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Update each tournament game with ESPN results
|
|
182
|
+
for (const game of games) {
|
|
183
|
+
const result = espnResults.get(game.espn_game_id);
|
|
184
|
+
|
|
185
|
+
if (!result) {
|
|
186
|
+
console.log(` Game ${game.espn_game_id}: Not found in ESPN data`);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (result.isFinal) {
|
|
191
|
+
console.log(` Game ${game.espn_game_id}: FINAL - ${result.team1Score} vs ${result.team2Score}`);
|
|
192
|
+
|
|
193
|
+
// Determine winner
|
|
194
|
+
let winnerId = result.winnerId;
|
|
195
|
+
if (!winnerId) {
|
|
196
|
+
// Fallback: higher score wins
|
|
197
|
+
winnerId = result.team1Score > result.team2Score ? game.team1_id : game.team2_id;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
await pool.query(
|
|
201
|
+
`UPDATE survivor_tournament_games
|
|
202
|
+
SET winner_id = $3, team1_score = $4, team2_score = $5, status = 'final', updated_at = NOW()
|
|
203
|
+
WHERE pool_id = $1 AND espn_game_id = $2`,
|
|
204
|
+
[poolId, game.espn_game_id, winnerId, result.team1Score, result.team2Score]
|
|
205
|
+
);
|
|
206
|
+
} else {
|
|
207
|
+
console.log(` Game ${game.espn_game_id}: ${result.status || 'In Progress'}`);
|
|
208
|
+
|
|
209
|
+
// Update live scores
|
|
210
|
+
const newStatus = result.status === 'In Progress' ? 'live' : 'scheduled';
|
|
211
|
+
await pool.query(
|
|
212
|
+
`UPDATE survivor_tournament_games
|
|
213
|
+
SET team1_score = $3, team2_score = $4, status = $5, updated_at = NOW()
|
|
214
|
+
WHERE pool_id = $1 AND espn_game_id = $2`,
|
|
215
|
+
[poolId, game.espn_game_id, result.team1Score, result.team2Score, newStatus]
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
} catch (error) {
|
|
220
|
+
console.error('[SurvivorOracle] Error fetching ESPN scores:', error.message);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Resolve the current round - update picks and eliminate losers
|
|
226
|
+
*/
|
|
227
|
+
async resolveCurrentRound(poolId) {
|
|
228
|
+
const client = await pool.connect();
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
await client.query('BEGIN');
|
|
232
|
+
|
|
233
|
+
// Get pool info
|
|
234
|
+
const poolResult = await client.query(
|
|
235
|
+
'SELECT * FROM survivor_pools WHERE id = $1 FOR UPDATE',
|
|
236
|
+
[poolId]
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
if (poolResult.rows.length === 0) {
|
|
240
|
+
throw new Error('Pool not found');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const survivorPool = poolResult.rows[0];
|
|
244
|
+
const currentRound = survivorPool.current_round;
|
|
245
|
+
|
|
246
|
+
console.log(`[SurvivorOracle] Resolving round ${currentRound} for pool ${poolId}`);
|
|
247
|
+
|
|
248
|
+
// Get all final games for this round
|
|
249
|
+
const gamesResult = await client.query(
|
|
250
|
+
`SELECT * FROM survivor_tournament_games
|
|
251
|
+
WHERE pool_id = $1 AND round = $2 AND status = 'final'`,
|
|
252
|
+
[poolId, currentRound]
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
const finalGames = gamesResult.rows;
|
|
256
|
+
const winnerMap = new Map();
|
|
257
|
+
|
|
258
|
+
for (const game of finalGames) {
|
|
259
|
+
// Map both team IDs to whether they won
|
|
260
|
+
winnerMap.set(game.team1_id, game.winner_id === game.team1_id);
|
|
261
|
+
winnerMap.set(game.team2_id, game.winner_id === game.team2_id);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Get all picks for this round
|
|
265
|
+
const picksResult = await client.query(
|
|
266
|
+
`SELECT sp.*, se.id as entry_id, se.user_id
|
|
267
|
+
FROM survivor_picks sp
|
|
268
|
+
JOIN survivor_entries se ON se.id = sp.entry_id
|
|
269
|
+
WHERE se.pool_id = $1 AND sp.round = $2 AND se.is_alive = true`,
|
|
270
|
+
[poolId, currentRound]
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
const picks = picksResult.rows;
|
|
274
|
+
let eliminatedCount = 0;
|
|
275
|
+
let survivedCount = 0;
|
|
276
|
+
|
|
277
|
+
// Process each pick
|
|
278
|
+
for (const pick of picks) {
|
|
279
|
+
const teamWon = winnerMap.get(pick.team_id);
|
|
280
|
+
|
|
281
|
+
if (teamWon === true) {
|
|
282
|
+
// Team won - mark pick as won
|
|
283
|
+
await client.query(
|
|
284
|
+
`UPDATE survivor_picks SET result = 'won', updated_at = NOW() WHERE id = $1`,
|
|
285
|
+
[pick.id]
|
|
286
|
+
);
|
|
287
|
+
survivedCount++;
|
|
288
|
+
console.log(` Entry ${pick.entry_id}: ${pick.team_name} WON - survived`);
|
|
289
|
+
} else if (teamWon === false) {
|
|
290
|
+
// Team lost - mark pick as lost and eliminate entry
|
|
291
|
+
await client.query(
|
|
292
|
+
`UPDATE survivor_picks SET result = 'lost', updated_at = NOW() WHERE id = $1`,
|
|
293
|
+
[pick.id]
|
|
294
|
+
);
|
|
295
|
+
await client.query(
|
|
296
|
+
`UPDATE survivor_entries
|
|
297
|
+
SET is_alive = false, eliminated_at_round = $2, updated_at = NOW()
|
|
298
|
+
WHERE id = $1`,
|
|
299
|
+
[pick.entry_id, currentRound]
|
|
300
|
+
);
|
|
301
|
+
eliminatedCount++;
|
|
302
|
+
console.log(` Entry ${pick.entry_id}: ${pick.team_name} LOST - ELIMINATED`);
|
|
303
|
+
} else {
|
|
304
|
+
// Game result not found (shouldn't happen if all games are final)
|
|
305
|
+
console.warn(` Entry ${pick.entry_id}: Team ${pick.team_id} result not found`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Eliminate entries that didn't submit a pick (auto-elimination)
|
|
310
|
+
const missingPicksResult = await client.query(
|
|
311
|
+
`UPDATE survivor_entries
|
|
312
|
+
SET is_alive = false, eliminated_at_round = $2, updated_at = NOW()
|
|
313
|
+
WHERE pool_id = $1
|
|
314
|
+
AND is_alive = true
|
|
315
|
+
AND id NOT IN (
|
|
316
|
+
SELECT entry_id FROM survivor_picks WHERE round = $2
|
|
317
|
+
)
|
|
318
|
+
RETURNING id`,
|
|
319
|
+
[poolId, currentRound]
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
const autoEliminatedCount = missingPicksResult.rowCount;
|
|
323
|
+
if (autoEliminatedCount > 0) {
|
|
324
|
+
console.log(` Auto-eliminated ${autoEliminatedCount} entries (no pick submitted)`);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Check remaining survivors
|
|
328
|
+
const aliveResult = await client.query(
|
|
329
|
+
'SELECT COUNT(*) as count FROM survivor_entries WHERE pool_id = $1 AND is_alive = true',
|
|
330
|
+
[poolId]
|
|
331
|
+
);
|
|
332
|
+
const aliveCount = parseInt(aliveResult.rows[0].count);
|
|
333
|
+
|
|
334
|
+
console.log(`[SurvivorOracle] Round ${currentRound} resolved:`);
|
|
335
|
+
console.log(` Survived: ${survivedCount}`);
|
|
336
|
+
console.log(` Eliminated: ${eliminatedCount}`);
|
|
337
|
+
console.log(` Auto-eliminated: ${autoEliminatedCount}`);
|
|
338
|
+
console.log(` Still alive: ${aliveCount}`);
|
|
339
|
+
|
|
340
|
+
// Determine next action
|
|
341
|
+
if (currentRound === 7 || aliveCount === 0) {
|
|
342
|
+
// Championship finished or everyone eliminated - complete the pool
|
|
343
|
+
await client.query(
|
|
344
|
+
`UPDATE survivor_pools SET status = 'complete', updated_at = NOW() WHERE id = $1`,
|
|
345
|
+
[poolId]
|
|
346
|
+
);
|
|
347
|
+
console.log(`[SurvivorOracle] Pool ${poolId} COMPLETE`);
|
|
348
|
+
|
|
349
|
+
// TODO: Trigger payout distribution
|
|
350
|
+
// This will call the Solana contract to distribute winnings
|
|
351
|
+
|
|
352
|
+
} else if (aliveCount === 1) {
|
|
353
|
+
// Only one survivor - they win!
|
|
354
|
+
await client.query(
|
|
355
|
+
`UPDATE survivor_pools SET status = 'complete', updated_at = NOW() WHERE id = $1`,
|
|
356
|
+
[poolId]
|
|
357
|
+
);
|
|
358
|
+
console.log(`[SurvivorOracle] Pool ${poolId} COMPLETE - Single winner!`);
|
|
359
|
+
|
|
360
|
+
// TODO: Trigger payout to single winner
|
|
361
|
+
|
|
362
|
+
} else {
|
|
363
|
+
// Advance to next round
|
|
364
|
+
const nextRound = currentRound + 1;
|
|
365
|
+
await client.query(
|
|
366
|
+
`UPDATE survivor_pools
|
|
367
|
+
SET current_round = $2, round_deadline = NULL, updated_at = NOW()
|
|
368
|
+
WHERE id = $1`,
|
|
369
|
+
[poolId, nextRound]
|
|
370
|
+
);
|
|
371
|
+
console.log(`[SurvivorOracle] Pool ${poolId} advanced to round ${nextRound} (${ROUND_NAMES[nextRound]})`);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
await client.query('COMMIT');
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
poolId,
|
|
378
|
+
round: currentRound,
|
|
379
|
+
survived: survivedCount,
|
|
380
|
+
eliminated: eliminatedCount,
|
|
381
|
+
autoEliminated: autoEliminatedCount,
|
|
382
|
+
stillAlive: aliveCount,
|
|
383
|
+
isComplete: currentRound === 7 || aliveCount <= 1
|
|
384
|
+
};
|
|
385
|
+
} catch (error) {
|
|
386
|
+
await client.query('ROLLBACK');
|
|
387
|
+
console.error('[SurvivorOracle] Error resolving round:', error.message);
|
|
388
|
+
throw error;
|
|
389
|
+
} finally {
|
|
390
|
+
client.release();
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Get survivors for payout (called when pool is complete)
|
|
396
|
+
*/
|
|
397
|
+
async getSurvivorsForPayout(poolId) {
|
|
398
|
+
const result = await pool.query(
|
|
399
|
+
`SELECT se.*, u.wallet_address, u.username
|
|
400
|
+
FROM survivor_entries se
|
|
401
|
+
JOIN users u ON u.id = se.user_id
|
|
402
|
+
WHERE se.pool_id = $1 AND se.is_alive = true`,
|
|
403
|
+
[poolId]
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
return result.rows;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Calculate payouts for survivors
|
|
411
|
+
*/
|
|
412
|
+
async calculatePayouts(poolId) {
|
|
413
|
+
// Get pool info
|
|
414
|
+
const poolResult = await pool.query(
|
|
415
|
+
'SELECT * FROM survivor_pools WHERE id = $1',
|
|
416
|
+
[poolId]
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
if (poolResult.rows.length === 0) {
|
|
420
|
+
throw new Error('Pool not found');
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const survivorPool = poolResult.rows[0];
|
|
424
|
+
|
|
425
|
+
// Get all entries for total pot
|
|
426
|
+
const entriesResult = await pool.query(
|
|
427
|
+
'SELECT COUNT(*) as count FROM survivor_entries WHERE pool_id = $1',
|
|
428
|
+
[poolId]
|
|
429
|
+
);
|
|
430
|
+
const totalEntries = parseInt(entriesResult.rows[0].count);
|
|
431
|
+
|
|
432
|
+
// Get survivors
|
|
433
|
+
const survivors = await this.getSurvivorsForPayout(poolId);
|
|
434
|
+
const survivorCount = survivors.length;
|
|
435
|
+
|
|
436
|
+
// Calculate payouts
|
|
437
|
+
const buyInLamports = BigInt(survivorPool.buy_in_lamports);
|
|
438
|
+
const totalPotLamports = buyInLamports * BigInt(totalEntries);
|
|
439
|
+
const feePercentage = 6; // 6% total
|
|
440
|
+
const feeLamports = totalPotLamports * BigInt(feePercentage) / BigInt(100);
|
|
441
|
+
const netPotLamports = totalPotLamports - feeLamports;
|
|
442
|
+
const perSurvivorLamports = survivorCount > 0
|
|
443
|
+
? netPotLamports / BigInt(survivorCount)
|
|
444
|
+
: BigInt(0);
|
|
445
|
+
|
|
446
|
+
return {
|
|
447
|
+
poolId,
|
|
448
|
+
totalEntries,
|
|
449
|
+
survivorCount,
|
|
450
|
+
totalPotLamports: totalPotLamports.toString(),
|
|
451
|
+
feeLamports: feeLamports.toString(),
|
|
452
|
+
netPotLamports: netPotLamports.toString(),
|
|
453
|
+
perSurvivorLamports: perSurvivorLamports.toString(),
|
|
454
|
+
survivors: survivors.map(s => ({
|
|
455
|
+
entryId: s.id,
|
|
456
|
+
userId: s.user_id,
|
|
457
|
+
walletAddress: s.wallet_address,
|
|
458
|
+
username: s.username,
|
|
459
|
+
payoutLamports: perSurvivorLamports.toString()
|
|
460
|
+
}))
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Export singleton instance
|
|
466
|
+
const survivorOracle = new SurvivorOracle();
|
|
467
|
+
|
|
468
|
+
module.exports = survivorOracle;
|
|
469
|
+
module.exports.SurvivorOracle = SurvivorOracle;
|