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,623 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Survivor Pool Controller
|
|
3
|
+
* Business logic for March Madness Survivor Pool feature
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { pool } = require('../services/db');
|
|
7
|
+
|
|
8
|
+
const LAMPORTS_PER_SOL = 1_000_000_000;
|
|
9
|
+
|
|
10
|
+
// Round names for display
|
|
11
|
+
const ROUND_NAMES = {
|
|
12
|
+
1: 'First Four',
|
|
13
|
+
2: 'Round of 64',
|
|
14
|
+
3: 'Round of 32',
|
|
15
|
+
4: 'Sweet 16',
|
|
16
|
+
5: 'Elite 8',
|
|
17
|
+
6: 'Final Four',
|
|
18
|
+
7: 'Championship'
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Create a new survivor pool
|
|
23
|
+
*/
|
|
24
|
+
async function createPool({
|
|
25
|
+
name,
|
|
26
|
+
year,
|
|
27
|
+
buyInLamports,
|
|
28
|
+
roundDeadline,
|
|
29
|
+
allowSameTeamTwice = false,
|
|
30
|
+
solanaGameId = null,
|
|
31
|
+
createdBy
|
|
32
|
+
}) {
|
|
33
|
+
const result = await pool.query(
|
|
34
|
+
`INSERT INTO survivor_pools
|
|
35
|
+
(name, year, buy_in_lamports, round_deadline, allow_same_team_twice, solana_game_id, created_by)
|
|
36
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
37
|
+
RETURNING *`,
|
|
38
|
+
[name, year, buyInLamports, roundDeadline, allowSameTeamTwice, solanaGameId, createdBy]
|
|
39
|
+
);
|
|
40
|
+
return result.rows[0];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get all pools with optional status filter
|
|
45
|
+
*/
|
|
46
|
+
async function getPools({ status, year } = {}) {
|
|
47
|
+
let query = `
|
|
48
|
+
SELECT
|
|
49
|
+
sp.*,
|
|
50
|
+
(SELECT COUNT(*) FROM survivor_entries WHERE pool_id = sp.id) as total_entries,
|
|
51
|
+
(SELECT COUNT(*) FROM survivor_entries WHERE pool_id = sp.id AND is_alive = true) as alive_count
|
|
52
|
+
FROM survivor_pools sp
|
|
53
|
+
WHERE 1=1
|
|
54
|
+
`;
|
|
55
|
+
const params = [];
|
|
56
|
+
|
|
57
|
+
if (status) {
|
|
58
|
+
params.push(status);
|
|
59
|
+
query += ` AND sp.status = $${params.length}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (year) {
|
|
63
|
+
params.push(year);
|
|
64
|
+
query += ` AND sp.year = $${params.length}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
query += ' ORDER BY sp.created_at DESC';
|
|
68
|
+
|
|
69
|
+
const result = await pool.query(query, params);
|
|
70
|
+
return result.rows.map(formatPool);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get a single pool by ID with detailed stats
|
|
75
|
+
*/
|
|
76
|
+
async function getPoolById(poolId) {
|
|
77
|
+
const result = await pool.query(
|
|
78
|
+
`SELECT
|
|
79
|
+
sp.*,
|
|
80
|
+
(SELECT COUNT(*) FROM survivor_entries WHERE pool_id = sp.id) as total_entries,
|
|
81
|
+
(SELECT COUNT(*) FROM survivor_entries WHERE pool_id = sp.id AND is_alive = true) as alive_count,
|
|
82
|
+
(SELECT SUM(buy_in_lamports) FROM survivor_pools WHERE id = sp.id) *
|
|
83
|
+
(SELECT COUNT(*) FROM survivor_entries WHERE pool_id = sp.id) as total_pot_lamports
|
|
84
|
+
FROM survivor_pools sp
|
|
85
|
+
WHERE sp.id = $1`,
|
|
86
|
+
[poolId]
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
if (result.rows.length === 0) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return formatPool(result.rows[0]);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Update pool (admin)
|
|
98
|
+
*/
|
|
99
|
+
async function updatePool(poolId, updates) {
|
|
100
|
+
const allowedFields = ['name', 'current_round', 'round_deadline', 'status', 'solana_game_id'];
|
|
101
|
+
const setClause = [];
|
|
102
|
+
const params = [poolId];
|
|
103
|
+
|
|
104
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
105
|
+
const snakeKey = key.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
|
|
106
|
+
if (allowedFields.includes(snakeKey)) {
|
|
107
|
+
params.push(value);
|
|
108
|
+
setClause.push(`${snakeKey} = $${params.length}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (setClause.length === 0) {
|
|
113
|
+
throw new Error('No valid fields to update');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
setClause.push('updated_at = NOW()');
|
|
117
|
+
|
|
118
|
+
const result = await pool.query(
|
|
119
|
+
`UPDATE survivor_pools SET ${setClause.join(', ')} WHERE id = $1 RETURNING *`,
|
|
120
|
+
params
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
return result.rows[0] ? formatPool(result.rows[0]) : null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Join a survivor pool
|
|
128
|
+
*/
|
|
129
|
+
async function joinPool({ poolId, userId, walletAddress, txSignature }) {
|
|
130
|
+
// Check pool exists and is open
|
|
131
|
+
const poolResult = await pool.query(
|
|
132
|
+
'SELECT * FROM survivor_pools WHERE id = $1',
|
|
133
|
+
[poolId]
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
if (poolResult.rows.length === 0) {
|
|
137
|
+
throw new Error('Pool not found');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const survivorPool = poolResult.rows[0];
|
|
141
|
+
|
|
142
|
+
// Allow joining when pool is 'open' (before bracket)
|
|
143
|
+
// Or 'active' but only if no games have been completed yet (tournament hasn't really started)
|
|
144
|
+
if (survivorPool.status === 'open') {
|
|
145
|
+
// Always allow when open
|
|
146
|
+
console.log(`[Survivor] Pool ${poolId} is open, allowing join`);
|
|
147
|
+
} else if (survivorPool.status === 'active') {
|
|
148
|
+
// Check if any games have been completed
|
|
149
|
+
const completedGames = await pool.query(
|
|
150
|
+
`SELECT COUNT(*) as count FROM survivor_tournament_games
|
|
151
|
+
WHERE pool_id = $1 AND status = 'final'`,
|
|
152
|
+
[poolId]
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const completedCount = parseInt(completedGames.rows[0].count);
|
|
156
|
+
console.log(`[Survivor] Pool ${poolId} is active, completed games: ${completedCount}`);
|
|
157
|
+
|
|
158
|
+
if (completedCount > 0) {
|
|
159
|
+
throw new Error(`Tournament has started (${completedCount} games completed). Reset the pool to allow new entries.`);
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
throw new Error(`Pool is ${survivorPool.status}, cannot join`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Check if user already joined
|
|
166
|
+
const existingEntry = await pool.query(
|
|
167
|
+
'SELECT id FROM survivor_entries WHERE pool_id = $1 AND user_id = $2',
|
|
168
|
+
[poolId, userId]
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
if (existingEntry.rows.length > 0) {
|
|
172
|
+
throw new Error('Already joined this pool');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Create entry
|
|
176
|
+
const result = await pool.query(
|
|
177
|
+
`INSERT INTO survivor_entries (pool_id, user_id, wallet_address, entry_tx_signature)
|
|
178
|
+
VALUES ($1, $2, $3, $4)
|
|
179
|
+
RETURNING *`,
|
|
180
|
+
[poolId, userId, walletAddress, txSignature]
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
return result.rows[0];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Get user's entry in a pool
|
|
188
|
+
*/
|
|
189
|
+
async function getUserEntry(poolId, userId) {
|
|
190
|
+
const result = await pool.query(
|
|
191
|
+
`SELECT
|
|
192
|
+
se.*,
|
|
193
|
+
u.username,
|
|
194
|
+
u.avatar
|
|
195
|
+
FROM survivor_entries se
|
|
196
|
+
JOIN users u ON u.id = se.user_id
|
|
197
|
+
WHERE se.pool_id = $1 AND se.user_id = $2`,
|
|
198
|
+
[poolId, userId]
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
if (result.rows.length === 0) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const entry = result.rows[0];
|
|
206
|
+
|
|
207
|
+
// Get all picks for this entry
|
|
208
|
+
const picksResult = await pool.query(
|
|
209
|
+
`SELECT * FROM survivor_picks WHERE entry_id = $1 ORDER BY round`,
|
|
210
|
+
[entry.id]
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
...entry,
|
|
215
|
+
picks: picksResult.rows.map(formatPick)
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Get available teams for current round
|
|
221
|
+
* Returns teams playing in the current round, marking previously picked teams as unavailable
|
|
222
|
+
*/
|
|
223
|
+
async function getAvailableTeams(poolId, userId) {
|
|
224
|
+
// Get pool info
|
|
225
|
+
const poolResult = await pool.query(
|
|
226
|
+
'SELECT * FROM survivor_pools WHERE id = $1',
|
|
227
|
+
[poolId]
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
if (poolResult.rows.length === 0) {
|
|
231
|
+
throw new Error('Pool not found');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const survivorPool = poolResult.rows[0];
|
|
235
|
+
const currentRound = survivorPool.current_round;
|
|
236
|
+
|
|
237
|
+
// Get user's entry
|
|
238
|
+
const entryResult = await pool.query(
|
|
239
|
+
'SELECT id FROM survivor_entries WHERE pool_id = $1 AND user_id = $2',
|
|
240
|
+
[poolId, userId]
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
if (entryResult.rows.length === 0) {
|
|
244
|
+
throw new Error('Not joined this pool');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const entryId = entryResult.rows[0].id;
|
|
248
|
+
|
|
249
|
+
// Get previously picked teams (to mark as unavailable)
|
|
250
|
+
const previousPicksResult = await pool.query(
|
|
251
|
+
'SELECT team_id FROM survivor_picks WHERE entry_id = $1',
|
|
252
|
+
[entryId]
|
|
253
|
+
);
|
|
254
|
+
const pickedTeamIds = new Set(previousPicksResult.rows.map(r => r.team_id));
|
|
255
|
+
|
|
256
|
+
// Get all games for current round
|
|
257
|
+
const gamesResult = await pool.query(
|
|
258
|
+
`SELECT * FROM survivor_tournament_games
|
|
259
|
+
WHERE pool_id = $1 AND round = $2
|
|
260
|
+
ORDER BY game_date, id`,
|
|
261
|
+
[poolId, currentRound]
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
console.log(`[Survivor] getAvailableTeams: poolId=${poolId}, currentRound=${currentRound}, games found: ${gamesResult.rows.length}`);
|
|
265
|
+
if (gamesResult.rows.length === 0) {
|
|
266
|
+
// Debug: check what rounds exist
|
|
267
|
+
const allGames = await pool.query(
|
|
268
|
+
`SELECT round, COUNT(*) as count FROM survivor_tournament_games WHERE pool_id = $1 GROUP BY round ORDER BY round`,
|
|
269
|
+
[poolId]
|
|
270
|
+
);
|
|
271
|
+
console.log(`[Survivor] Available rounds in DB:`, allGames.rows);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Build team list with availability
|
|
275
|
+
const teams = [];
|
|
276
|
+
for (const game of gamesResult.rows) {
|
|
277
|
+
const team1Available = !pickedTeamIds.has(game.team1_id) || survivorPool.allow_same_team_twice;
|
|
278
|
+
const team2Available = !pickedTeamIds.has(game.team2_id) || survivorPool.allow_same_team_twice;
|
|
279
|
+
|
|
280
|
+
teams.push({
|
|
281
|
+
teamId: game.team1_id,
|
|
282
|
+
teamName: game.team1_name,
|
|
283
|
+
seed: game.team1_seed,
|
|
284
|
+
logo: game.team1_logo,
|
|
285
|
+
available: team1Available,
|
|
286
|
+
espnGameId: game.espn_game_id,
|
|
287
|
+
gameDate: game.game_date,
|
|
288
|
+
opponent: {
|
|
289
|
+
teamId: game.team2_id,
|
|
290
|
+
teamName: game.team2_name,
|
|
291
|
+
seed: game.team2_seed
|
|
292
|
+
},
|
|
293
|
+
region: game.region
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
teams.push({
|
|
297
|
+
teamId: game.team2_id,
|
|
298
|
+
teamName: game.team2_name,
|
|
299
|
+
seed: game.team2_seed,
|
|
300
|
+
logo: game.team2_logo,
|
|
301
|
+
available: team2Available,
|
|
302
|
+
espnGameId: game.espn_game_id,
|
|
303
|
+
gameDate: game.game_date,
|
|
304
|
+
opponent: {
|
|
305
|
+
teamId: game.team1_id,
|
|
306
|
+
teamName: game.team1_name,
|
|
307
|
+
seed: game.team1_seed
|
|
308
|
+
},
|
|
309
|
+
region: game.region
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
round: currentRound,
|
|
315
|
+
roundName: ROUND_NAMES[currentRound],
|
|
316
|
+
deadline: survivorPool.round_deadline,
|
|
317
|
+
teams
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Submit a pick for the current round
|
|
323
|
+
*/
|
|
324
|
+
async function submitPick({ poolId, userId, teamId, teamName, teamSeed, teamLogo, espnGameId, opponentName, opponentSeed }) {
|
|
325
|
+
// Get pool
|
|
326
|
+
const poolResult = await pool.query(
|
|
327
|
+
'SELECT * FROM survivor_pools WHERE id = $1',
|
|
328
|
+
[poolId]
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
if (poolResult.rows.length === 0) {
|
|
332
|
+
throw new Error('Pool not found');
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const survivorPool = poolResult.rows[0];
|
|
336
|
+
|
|
337
|
+
// Check pool is active
|
|
338
|
+
if (survivorPool.status !== 'active' && survivorPool.status !== 'open') {
|
|
339
|
+
throw new Error(`Pool is ${survivorPool.status}, cannot submit picks`);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Check deadline
|
|
343
|
+
if (survivorPool.round_deadline && new Date() > new Date(survivorPool.round_deadline)) {
|
|
344
|
+
throw new Error('Pick deadline has passed');
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Get user's entry
|
|
348
|
+
const entryResult = await pool.query(
|
|
349
|
+
'SELECT * FROM survivor_entries WHERE pool_id = $1 AND user_id = $2',
|
|
350
|
+
[poolId, userId]
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
if (entryResult.rows.length === 0) {
|
|
354
|
+
throw new Error('Not joined this pool');
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const entry = entryResult.rows[0];
|
|
358
|
+
|
|
359
|
+
// Check if eliminated
|
|
360
|
+
if (!entry.is_alive) {
|
|
361
|
+
throw new Error('You have been eliminated from this pool');
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Check if team was already picked (if not allowed)
|
|
365
|
+
if (!survivorPool.allow_same_team_twice) {
|
|
366
|
+
const existingPick = await pool.query(
|
|
367
|
+
'SELECT id FROM survivor_picks WHERE entry_id = $1 AND team_id = $2',
|
|
368
|
+
[entry.id, teamId]
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
if (existingPick.rows.length > 0) {
|
|
372
|
+
throw new Error('You have already picked this team in a previous round');
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Upsert pick for current round (allows changing pick before deadline)
|
|
377
|
+
const result = await pool.query(
|
|
378
|
+
`INSERT INTO survivor_picks
|
|
379
|
+
(entry_id, round, team_id, team_name, team_seed, team_logo, espn_game_id, opponent_name, opponent_seed)
|
|
380
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
|
381
|
+
ON CONFLICT (entry_id, round)
|
|
382
|
+
DO UPDATE SET
|
|
383
|
+
team_id = EXCLUDED.team_id,
|
|
384
|
+
team_name = EXCLUDED.team_name,
|
|
385
|
+
team_seed = EXCLUDED.team_seed,
|
|
386
|
+
team_logo = EXCLUDED.team_logo,
|
|
387
|
+
espn_game_id = EXCLUDED.espn_game_id,
|
|
388
|
+
opponent_name = EXCLUDED.opponent_name,
|
|
389
|
+
opponent_seed = EXCLUDED.opponent_seed,
|
|
390
|
+
updated_at = NOW()
|
|
391
|
+
RETURNING *`,
|
|
392
|
+
[entry.id, survivorPool.current_round, teamId, teamName, teamSeed, teamLogo, espnGameId, opponentName, opponentSeed]
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
return formatPick(result.rows[0]);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Get all picks for a user's entry
|
|
400
|
+
*/
|
|
401
|
+
async function getUserPicks(poolId, userId) {
|
|
402
|
+
const result = await pool.query(
|
|
403
|
+
`SELECT sp.*
|
|
404
|
+
FROM survivor_picks sp
|
|
405
|
+
JOIN survivor_entries se ON se.id = sp.entry_id
|
|
406
|
+
WHERE se.pool_id = $1 AND se.user_id = $2
|
|
407
|
+
ORDER BY sp.round`,
|
|
408
|
+
[poolId, userId]
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
return result.rows.map(formatPick);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Get leaderboard - surviving entries
|
|
416
|
+
*/
|
|
417
|
+
async function getAliveEntries(poolId) {
|
|
418
|
+
// Get current round first
|
|
419
|
+
const poolResult = await pool.query(
|
|
420
|
+
`SELECT current_round FROM survivor_pools WHERE id = $1`,
|
|
421
|
+
[poolId]
|
|
422
|
+
);
|
|
423
|
+
const currentRound = poolResult.rows[0]?.current_round || 1;
|
|
424
|
+
|
|
425
|
+
const result = await pool.query(
|
|
426
|
+
`SELECT
|
|
427
|
+
se.*,
|
|
428
|
+
u.username,
|
|
429
|
+
u.avatar,
|
|
430
|
+
(SELECT COUNT(*) FROM survivor_picks WHERE entry_id = se.id AND result = 'won') as wins,
|
|
431
|
+
sp.team_name as current_pick_team,
|
|
432
|
+
sp.team_logo as current_pick_logo,
|
|
433
|
+
sp.team_id as current_pick_team_id
|
|
434
|
+
FROM survivor_entries se
|
|
435
|
+
JOIN users u ON u.id = se.user_id
|
|
436
|
+
LEFT JOIN survivor_picks sp ON sp.entry_id = se.id AND sp.round = $2
|
|
437
|
+
WHERE se.pool_id = $1 AND se.is_alive = true
|
|
438
|
+
ORDER BY wins DESC, se.created_at`,
|
|
439
|
+
[poolId, currentRound]
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
return result.rows;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Get eliminated entries
|
|
447
|
+
*/
|
|
448
|
+
async function getEliminatedEntries(poolId) {
|
|
449
|
+
const result = await pool.query(
|
|
450
|
+
`SELECT
|
|
451
|
+
se.*,
|
|
452
|
+
u.username,
|
|
453
|
+
u.avatar,
|
|
454
|
+
sp.team_name as eliminated_by_pick,
|
|
455
|
+
sp.team_logo as eliminated_by_logo,
|
|
456
|
+
sp.team_id as eliminated_by_team_id,
|
|
457
|
+
(SELECT COUNT(*) FROM survivor_picks WHERE entry_id = se.id AND result = 'won') as wins
|
|
458
|
+
FROM survivor_entries se
|
|
459
|
+
JOIN users u ON u.id = se.user_id
|
|
460
|
+
LEFT JOIN survivor_picks sp ON sp.entry_id = se.id AND sp.round = se.eliminated_at_round
|
|
461
|
+
WHERE se.pool_id = $1 AND se.is_alive = false
|
|
462
|
+
ORDER BY se.eliminated_at_round DESC, se.updated_at DESC`,
|
|
463
|
+
[poolId]
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
return result.rows;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Get pool stats summary
|
|
471
|
+
*/
|
|
472
|
+
async function getPoolStats(poolId) {
|
|
473
|
+
const result = await pool.query(
|
|
474
|
+
`SELECT
|
|
475
|
+
(SELECT COUNT(*) FROM survivor_entries WHERE pool_id = $1) as total_entries,
|
|
476
|
+
(SELECT COUNT(*) FROM survivor_entries WHERE pool_id = $1 AND is_alive = true) as alive_count,
|
|
477
|
+
(SELECT COUNT(*) FROM survivor_entries WHERE pool_id = $1 AND is_alive = false) as eliminated_count,
|
|
478
|
+
(SELECT buy_in_lamports FROM survivor_pools WHERE id = $1) as buy_in_lamports,
|
|
479
|
+
(SELECT current_round FROM survivor_pools WHERE id = $1) as current_round,
|
|
480
|
+
(SELECT status FROM survivor_pools WHERE id = $1) as status
|
|
481
|
+
`,
|
|
482
|
+
[poolId]
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
const stats = result.rows[0];
|
|
486
|
+
const totalPotLamports = BigInt(stats.buy_in_lamports || 0) * BigInt(stats.total_entries || 0);
|
|
487
|
+
const feePercentage = 6; // 6% total fee
|
|
488
|
+
const netPotLamports = totalPotLamports - (totalPotLamports * BigInt(feePercentage) / BigInt(100));
|
|
489
|
+
|
|
490
|
+
return {
|
|
491
|
+
totalEntries: parseInt(stats.total_entries) || 0,
|
|
492
|
+
aliveCount: parseInt(stats.alive_count) || 0,
|
|
493
|
+
eliminatedCount: parseInt(stats.eliminated_count) || 0,
|
|
494
|
+
currentRound: stats.current_round,
|
|
495
|
+
roundName: ROUND_NAMES[stats.current_round],
|
|
496
|
+
status: stats.status,
|
|
497
|
+
totalPotLamports: totalPotLamports.toString(),
|
|
498
|
+
totalPotSol: Number(totalPotLamports) / LAMPORTS_PER_SOL,
|
|
499
|
+
netPotLamports: netPotLamports.toString(),
|
|
500
|
+
netPotSol: Number(netPotLamports) / LAMPORTS_PER_SOL,
|
|
501
|
+
potentialWinningsPerSurvivor: stats.alive_count > 0
|
|
502
|
+
? Number(netPotLamports) / LAMPORTS_PER_SOL / parseInt(stats.alive_count)
|
|
503
|
+
: 0
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Import tournament games from ESPN data
|
|
509
|
+
*/
|
|
510
|
+
async function importTournamentGames(poolId, games) {
|
|
511
|
+
const insertedGames = [];
|
|
512
|
+
|
|
513
|
+
for (const game of games) {
|
|
514
|
+
const result = await pool.query(
|
|
515
|
+
`INSERT INTO survivor_tournament_games
|
|
516
|
+
(pool_id, round, espn_game_id, game_date, team1_id, team1_name, team1_seed, team1_logo,
|
|
517
|
+
team2_id, team2_name, team2_seed, team2_logo, region, status)
|
|
518
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
|
|
519
|
+
ON CONFLICT (pool_id, espn_game_id) DO UPDATE SET
|
|
520
|
+
game_date = EXCLUDED.game_date,
|
|
521
|
+
team1_name = EXCLUDED.team1_name,
|
|
522
|
+
team2_name = EXCLUDED.team2_name,
|
|
523
|
+
status = EXCLUDED.status,
|
|
524
|
+
updated_at = NOW()
|
|
525
|
+
RETURNING *`,
|
|
526
|
+
[
|
|
527
|
+
poolId,
|
|
528
|
+
game.round,
|
|
529
|
+
game.espnGameId,
|
|
530
|
+
game.gameDate,
|
|
531
|
+
game.team1Id,
|
|
532
|
+
game.team1Name,
|
|
533
|
+
game.team1Seed,
|
|
534
|
+
game.team1Logo,
|
|
535
|
+
game.team2Id,
|
|
536
|
+
game.team2Name,
|
|
537
|
+
game.team2Seed,
|
|
538
|
+
game.team2Logo,
|
|
539
|
+
game.region,
|
|
540
|
+
game.status || 'scheduled'
|
|
541
|
+
]
|
|
542
|
+
);
|
|
543
|
+
|
|
544
|
+
insertedGames.push(result.rows[0]);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return insertedGames;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Update tournament game result
|
|
552
|
+
*/
|
|
553
|
+
async function updateGameResult(poolId, espnGameId, { winnerId, team1Score, team2Score, status }) {
|
|
554
|
+
const result = await pool.query(
|
|
555
|
+
`UPDATE survivor_tournament_games
|
|
556
|
+
SET winner_id = $3, team1_score = $4, team2_score = $5, status = $6, updated_at = NOW()
|
|
557
|
+
WHERE pool_id = $1 AND espn_game_id = $2
|
|
558
|
+
RETURNING *`,
|
|
559
|
+
[poolId, espnGameId, winnerId, team1Score, team2Score, status]
|
|
560
|
+
);
|
|
561
|
+
|
|
562
|
+
return result.rows[0];
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Helper function to format pick for API response
|
|
566
|
+
function formatPick(row) {
|
|
567
|
+
return {
|
|
568
|
+
id: row.id,
|
|
569
|
+
entryId: row.entry_id,
|
|
570
|
+
round: row.round,
|
|
571
|
+
teamId: row.team_id,
|
|
572
|
+
teamName: row.team_name,
|
|
573
|
+
teamLogo: row.team_logo,
|
|
574
|
+
espnGameId: row.espn_game_id,
|
|
575
|
+
opponentName: row.opponent_name,
|
|
576
|
+
opponentSeed: row.opponent_seed,
|
|
577
|
+
result: row.result,
|
|
578
|
+
createdAt: row.created_at,
|
|
579
|
+
updatedAt: row.updated_at
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Helper function to format pool for API response
|
|
584
|
+
function formatPool(row) {
|
|
585
|
+
return {
|
|
586
|
+
id: row.id,
|
|
587
|
+
name: row.name,
|
|
588
|
+
year: row.year,
|
|
589
|
+
buyInLamports: row.buy_in_lamports,
|
|
590
|
+
buyInSol: Number(row.buy_in_lamports) / LAMPORTS_PER_SOL,
|
|
591
|
+
currentRound: row.current_round,
|
|
592
|
+
roundName: ROUND_NAMES[row.current_round],
|
|
593
|
+
roundDeadline: row.round_deadline,
|
|
594
|
+
status: row.status,
|
|
595
|
+
allowSameTeamTwice: row.allow_same_team_twice,
|
|
596
|
+
solanaGameId: row.solana_game_id,
|
|
597
|
+
createdBy: row.created_by,
|
|
598
|
+
createdAt: row.created_at,
|
|
599
|
+
updatedAt: row.updated_at,
|
|
600
|
+
entryCount: parseInt(row.total_entries) || 0,
|
|
601
|
+
aliveCount: parseInt(row.alive_count) || 0,
|
|
602
|
+
totalPotLamports: row.total_pot_lamports,
|
|
603
|
+
totalPotSol: row.total_pot_lamports ? Number(row.total_pot_lamports) / LAMPORTS_PER_SOL : undefined
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
module.exports = {
|
|
608
|
+
createPool,
|
|
609
|
+
getPools,
|
|
610
|
+
getPoolById,
|
|
611
|
+
updatePool,
|
|
612
|
+
joinPool,
|
|
613
|
+
getUserEntry,
|
|
614
|
+
getAvailableTeams,
|
|
615
|
+
submitPick,
|
|
616
|
+
getUserPicks,
|
|
617
|
+
getAliveEntries,
|
|
618
|
+
getEliminatedEntries,
|
|
619
|
+
getPoolStats,
|
|
620
|
+
importTournamentGames,
|
|
621
|
+
updateGameResult,
|
|
622
|
+
ROUND_NAMES
|
|
623
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Oracle Monitor - Cron job for automatic sports game resolution
|
|
4
|
+
* Checks pending games every 1 minute and resolves completed ones
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
require('dotenv').config();
|
|
8
|
+
const { Keypair } = require('@solana/web3.js');
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const AutomaticGameOracle = require('../services/automaticGameOracle');
|
|
12
|
+
|
|
13
|
+
// Load oracle wallet
|
|
14
|
+
function loadOracleWallet() {
|
|
15
|
+
// Try environment variable first (for Heroku)
|
|
16
|
+
if (process.env.ORACLE_WALLET_JSON) {
|
|
17
|
+
console.log('š Loading oracle wallet from environment variable');
|
|
18
|
+
const secretKey = JSON.parse(process.env.ORACLE_WALLET_JSON);
|
|
19
|
+
return Keypair.fromSecretKey(Uint8Array.from(secretKey));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Fallback to file (for local development)
|
|
23
|
+
const oracleWalletPath = process.env.ORACLE_WALLET_PATH || path.join(__dirname, '../wallets/oracle.json');
|
|
24
|
+
|
|
25
|
+
if (!fs.existsSync(oracleWalletPath)) {
|
|
26
|
+
console.error(`ā Oracle wallet not found at: ${oracleWalletPath}`);
|
|
27
|
+
console.log('š” Create one with: solana-keygen new -o wallets/oracle.json');
|
|
28
|
+
console.log('š” Or set ORACLE_WALLET_JSON environment variable');
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.log('š Loading oracle wallet from file:', oracleWalletPath);
|
|
33
|
+
const secretKey = JSON.parse(fs.readFileSync(oracleWalletPath, 'utf-8'));
|
|
34
|
+
return Keypair.fromSecretKey(Uint8Array.from(secretKey));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Configuration
|
|
38
|
+
const config = {
|
|
39
|
+
rpcUrl: process.env.SOLANA_NETWORK || 'https://api.devnet.solana.com',
|
|
40
|
+
programId: process.env.PROGRAM_ID || '8DJTkgk6MDr6tPtw4v2VzYAz9WWvmCg6786vZrEK3o5q',
|
|
41
|
+
oracleKeypair: loadOracleWallet(),
|
|
42
|
+
liveScoresApiUrl: process.env.LIVE_SCORES_API_URL || 'http://localhost:3002',
|
|
43
|
+
// PostgreSQL dubs-server URL - Use PORT-based URL for local dev, explicit URL for production
|
|
44
|
+
dubsServerUrl: process.env.DUBS_SERVER_URL || `http://localhost:${process.env.PORT || 3001}`,
|
|
45
|
+
checkIntervalMs: parseInt(process.env.ORACLE_CHECK_INTERVAL || '60000'), // 1 minute
|
|
46
|
+
notifyBeforeMinutes: parseInt(process.env.NOTIFY_BEFORE_MINUTES || '10'), // Notify 10 minutes before
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
console.log('š¤ Initializing Oracle Monitor...');
|
|
50
|
+
console.log(` RPC: ${config.rpcUrl}`);
|
|
51
|
+
console.log(` Program ID: ${config.programId}`);
|
|
52
|
+
console.log(` Oracle: ${config.oracleKeypair.publicKey.toString()}`);
|
|
53
|
+
console.log(` PostgreSQL: ${config.dubsServerUrl}`);
|
|
54
|
+
console.log(` Live Scores API: ${config.liveScoresApiUrl}`);
|
|
55
|
+
console.log(` Check interval: ${config.checkIntervalMs / 1000}s`);
|
|
56
|
+
console.log(` Notify before: ${config.notifyBeforeMinutes} minutes`);
|
|
57
|
+
|
|
58
|
+
// Create and start oracle
|
|
59
|
+
const oracle = new AutomaticGameOracle(config);
|
|
60
|
+
oracle.start();
|
|
61
|
+
|
|
62
|
+
// Graceful shutdown
|
|
63
|
+
process.on('SIGINT', () => {
|
|
64
|
+
console.log('\nš Shutting down oracle monitor...');
|
|
65
|
+
oracle.stop();
|
|
66
|
+
process.exit(0);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
process.on('SIGTERM', () => {
|
|
70
|
+
console.log('\nš Shutting down oracle monitor...');
|
|
71
|
+
oracle.stop();
|
|
72
|
+
process.exit(0);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
console.log('ā
Oracle monitor started successfully!');
|
|
76
|
+
console.log(' Press Ctrl+C to stop\n');
|
|
77
|
+
|