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,554 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pick'em Controller
|
|
3
|
+
* Business logic for UFC Pick'em pools
|
|
4
|
+
* Pattern mirrors survivorController.js
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { pool } = require('../services/db');
|
|
8
|
+
|
|
9
|
+
const LAMPORTS_PER_SOL = 1_000_000_000;
|
|
10
|
+
const FEE_PERCENT = 6; // 5% operator + 1% oracle
|
|
11
|
+
|
|
12
|
+
// ========== HELPERS ==========
|
|
13
|
+
|
|
14
|
+
function formatPool(row) {
|
|
15
|
+
if (!row) return null;
|
|
16
|
+
const buyInSol = Number(row.buy_in_lamports) / LAMPORTS_PER_SOL;
|
|
17
|
+
const totalEntries = Number(row.total_entries || 0);
|
|
18
|
+
const totalPotLamports = totalEntries * Number(row.buy_in_lamports);
|
|
19
|
+
const netPotLamports = Math.floor(totalPotLamports * (1 - FEE_PERCENT / 100));
|
|
20
|
+
return {
|
|
21
|
+
id: row.id,
|
|
22
|
+
name: row.name,
|
|
23
|
+
espnEventId: row.espn_event_id,
|
|
24
|
+
eventDate: row.event_date,
|
|
25
|
+
buyInLamports: Number(row.buy_in_lamports),
|
|
26
|
+
buyInSol,
|
|
27
|
+
lockTime: row.lock_time,
|
|
28
|
+
status: row.status,
|
|
29
|
+
solanaGameId: row.solana_game_id,
|
|
30
|
+
solanaGameAddress: row.solana_game_address,
|
|
31
|
+
totalEntries,
|
|
32
|
+
totalPotLamports,
|
|
33
|
+
totalPotSol: totalPotLamports / LAMPORTS_PER_SOL,
|
|
34
|
+
netPotLamports,
|
|
35
|
+
netPotSol: netPotLamports / LAMPORTS_PER_SOL,
|
|
36
|
+
createdBy: row.created_by,
|
|
37
|
+
createdAt: row.created_at,
|
|
38
|
+
updatedAt: row.updated_at,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function formatFight(row) {
|
|
43
|
+
if (!row) return null;
|
|
44
|
+
return {
|
|
45
|
+
id: row.id,
|
|
46
|
+
poolId: row.pool_id,
|
|
47
|
+
espnCompetitionId: row.espn_competition_id,
|
|
48
|
+
fightOrder: row.fight_order,
|
|
49
|
+
fighterA: {
|
|
50
|
+
name: row.fighter_a_name,
|
|
51
|
+
headshot: row.fighter_a_headshot,
|
|
52
|
+
country: row.fighter_a_country,
|
|
53
|
+
record: row.fighter_a_record,
|
|
54
|
+
},
|
|
55
|
+
fighterB: {
|
|
56
|
+
name: row.fighter_b_name,
|
|
57
|
+
headshot: row.fighter_b_headshot,
|
|
58
|
+
country: row.fighter_b_country,
|
|
59
|
+
record: row.fighter_b_record,
|
|
60
|
+
},
|
|
61
|
+
weightClass: row.weight_class,
|
|
62
|
+
winner: row.winner,
|
|
63
|
+
method: row.method,
|
|
64
|
+
status: row.status,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function formatEntry(row) {
|
|
69
|
+
if (!row) return null;
|
|
70
|
+
return {
|
|
71
|
+
id: row.id,
|
|
72
|
+
poolId: row.pool_id,
|
|
73
|
+
userId: row.user_id,
|
|
74
|
+
walletAddress: row.wallet_address,
|
|
75
|
+
entryTxSignature: row.entry_tx_signature,
|
|
76
|
+
score: row.score,
|
|
77
|
+
rank: row.rank,
|
|
78
|
+
username: row.username || null,
|
|
79
|
+
avatar: row.avatar || null,
|
|
80
|
+
createdAt: row.created_at,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function formatPick(row) {
|
|
85
|
+
if (!row) return null;
|
|
86
|
+
return {
|
|
87
|
+
id: row.id,
|
|
88
|
+
entryId: row.entry_id,
|
|
89
|
+
fightId: row.fight_id,
|
|
90
|
+
pick: row.pick,
|
|
91
|
+
isCorrect: row.is_correct,
|
|
92
|
+
// Joined fight fields (if present)
|
|
93
|
+
fighterAName: row.fighter_a_name || null,
|
|
94
|
+
fighterBName: row.fighter_b_name || null,
|
|
95
|
+
weightClass: row.weight_class || null,
|
|
96
|
+
fightWinner: row.winner || null,
|
|
97
|
+
fightStatus: row.fight_status || null,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ========== POOL MANAGEMENT ==========
|
|
102
|
+
|
|
103
|
+
async function createPool({ name, espnEventId, eventDate, lockTime, solanaGameId, solanaGameAddress, createdBy }) {
|
|
104
|
+
const result = await pool.query(
|
|
105
|
+
`INSERT INTO pickem_pools (name, espn_event_id, event_date, lock_time, solana_game_id, solana_game_address, created_by)
|
|
106
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
107
|
+
RETURNING *`,
|
|
108
|
+
[name, espnEventId, eventDate, lockTime, solanaGameId || null, solanaGameAddress || null, createdBy || null]
|
|
109
|
+
);
|
|
110
|
+
return formatPool(result.rows[0]);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function getPools({ status } = {}) {
|
|
114
|
+
let query = 'SELECT * FROM pickem_pools';
|
|
115
|
+
const params = [];
|
|
116
|
+
if (status) {
|
|
117
|
+
query += ' WHERE status = $1';
|
|
118
|
+
params.push(status);
|
|
119
|
+
}
|
|
120
|
+
query += ' ORDER BY event_date DESC';
|
|
121
|
+
const result = await pool.query(query, params);
|
|
122
|
+
return result.rows.map(formatPool);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function getPoolById(poolId) {
|
|
126
|
+
const result = await pool.query('SELECT * FROM pickem_pools WHERE id = $1', [poolId]);
|
|
127
|
+
if (result.rows.length === 0) return null;
|
|
128
|
+
return formatPool(result.rows[0]);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function updatePool(poolId, updates) {
|
|
132
|
+
const allowed = ['name', 'lock_time', 'status', 'solana_game_id', 'solana_game_address', 'total_entries'];
|
|
133
|
+
const setClauses = [];
|
|
134
|
+
const params = [];
|
|
135
|
+
let idx = 1;
|
|
136
|
+
|
|
137
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
138
|
+
if (allowed.includes(key)) {
|
|
139
|
+
setClauses.push(`${key} = $${idx}`);
|
|
140
|
+
params.push(value);
|
|
141
|
+
idx++;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (setClauses.length === 0) return getPoolById(poolId);
|
|
145
|
+
|
|
146
|
+
setClauses.push(`updated_at = NOW()`);
|
|
147
|
+
params.push(poolId);
|
|
148
|
+
|
|
149
|
+
const result = await pool.query(
|
|
150
|
+
`UPDATE pickem_pools SET ${setClauses.join(', ')} WHERE id = $${idx} RETURNING *`,
|
|
151
|
+
params
|
|
152
|
+
);
|
|
153
|
+
return formatPool(result.rows[0]);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ========== FIGHT MANAGEMENT ==========
|
|
157
|
+
|
|
158
|
+
async function importFights(poolId, fights) {
|
|
159
|
+
const inserted = [];
|
|
160
|
+
for (const f of fights) {
|
|
161
|
+
const result = await pool.query(
|
|
162
|
+
`INSERT INTO pickem_fights (pool_id, espn_competition_id, fight_order,
|
|
163
|
+
fighter_a_name, fighter_a_headshot, fighter_a_country, fighter_a_record,
|
|
164
|
+
fighter_b_name, fighter_b_headshot, fighter_b_country, fighter_b_record,
|
|
165
|
+
weight_class)
|
|
166
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
|
167
|
+
ON CONFLICT (pool_id, espn_competition_id)
|
|
168
|
+
DO UPDATE SET
|
|
169
|
+
fight_order = EXCLUDED.fight_order,
|
|
170
|
+
fighter_a_name = EXCLUDED.fighter_a_name,
|
|
171
|
+
fighter_a_headshot = EXCLUDED.fighter_a_headshot,
|
|
172
|
+
fighter_a_country = EXCLUDED.fighter_a_country,
|
|
173
|
+
fighter_a_record = EXCLUDED.fighter_a_record,
|
|
174
|
+
fighter_b_name = EXCLUDED.fighter_b_name,
|
|
175
|
+
fighter_b_headshot = EXCLUDED.fighter_b_headshot,
|
|
176
|
+
fighter_b_country = EXCLUDED.fighter_b_country,
|
|
177
|
+
fighter_b_record = EXCLUDED.fighter_b_record,
|
|
178
|
+
weight_class = EXCLUDED.weight_class,
|
|
179
|
+
updated_at = NOW()
|
|
180
|
+
RETURNING *`,
|
|
181
|
+
[
|
|
182
|
+
poolId, f.espnCompetitionId, f.fightOrder,
|
|
183
|
+
f.fighterAName, f.fighterAHeadshot || null, f.fighterACountry || null, f.fighterARecord || null,
|
|
184
|
+
f.fighterBName, f.fighterBHeadshot || null, f.fighterBCountry || null, f.fighterBRecord || null,
|
|
185
|
+
f.weightClass || null,
|
|
186
|
+
]
|
|
187
|
+
);
|
|
188
|
+
inserted.push(formatFight(result.rows[0]));
|
|
189
|
+
}
|
|
190
|
+
return inserted;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function getFights(poolId) {
|
|
194
|
+
const result = await pool.query(
|
|
195
|
+
'SELECT * FROM pickem_fights WHERE pool_id = $1 ORDER BY fight_order ASC',
|
|
196
|
+
[poolId]
|
|
197
|
+
);
|
|
198
|
+
return result.rows.map(formatFight);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function updateFightResult(fightId, { winner, method, status }) {
|
|
202
|
+
const result = await pool.query(
|
|
203
|
+
`UPDATE pickem_fights SET winner = $1, method = $2, status = $3, updated_at = NOW()
|
|
204
|
+
WHERE id = $4 RETURNING *`,
|
|
205
|
+
[winner || null, method || null, status, fightId]
|
|
206
|
+
);
|
|
207
|
+
return result.rows[0] ? formatFight(result.rows[0]) : null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ========== ENTRY MANAGEMENT ==========
|
|
211
|
+
|
|
212
|
+
async function joinPool({ poolId, userId, walletAddress, txSignature }) {
|
|
213
|
+
const client = await pool.connect();
|
|
214
|
+
try {
|
|
215
|
+
await client.query('BEGIN');
|
|
216
|
+
|
|
217
|
+
// Check pool exists and is open
|
|
218
|
+
const poolResult = await client.query(
|
|
219
|
+
'SELECT * FROM pickem_pools WHERE id = $1',
|
|
220
|
+
[poolId]
|
|
221
|
+
);
|
|
222
|
+
if (poolResult.rows.length === 0) throw new Error('Pool not found');
|
|
223
|
+
const pickemPool = poolResult.rows[0];
|
|
224
|
+
if (pickemPool.status !== 'open') throw new Error(`Pool is ${pickemPool.status}, cannot join`);
|
|
225
|
+
|
|
226
|
+
// Insert entry
|
|
227
|
+
const entryResult = await client.query(
|
|
228
|
+
`INSERT INTO pickem_entries (pool_id, user_id, wallet_address, entry_tx_signature)
|
|
229
|
+
VALUES ($1, $2, $3, $4)
|
|
230
|
+
RETURNING *`,
|
|
231
|
+
[poolId, userId, walletAddress, txSignature || null]
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
// Increment total_entries
|
|
235
|
+
await client.query(
|
|
236
|
+
'UPDATE pickem_pools SET total_entries = total_entries + 1, updated_at = NOW() WHERE id = $1',
|
|
237
|
+
[poolId]
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
await client.query('COMMIT');
|
|
241
|
+
return formatEntry(entryResult.rows[0]);
|
|
242
|
+
} catch (error) {
|
|
243
|
+
await client.query('ROLLBACK');
|
|
244
|
+
throw error;
|
|
245
|
+
} finally {
|
|
246
|
+
client.release();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function getUserEntry(poolId, userId) {
|
|
251
|
+
const result = await pool.query(
|
|
252
|
+
`SELECT pe.*, u.username, u.avatar
|
|
253
|
+
FROM pickem_entries pe
|
|
254
|
+
LEFT JOIN users u ON pe.user_id = u.id
|
|
255
|
+
WHERE pe.pool_id = $1 AND pe.user_id = $2`,
|
|
256
|
+
[poolId, userId]
|
|
257
|
+
);
|
|
258
|
+
if (result.rows.length === 0) return null;
|
|
259
|
+
|
|
260
|
+
const entry = formatEntry(result.rows[0]);
|
|
261
|
+
|
|
262
|
+
// Attach picks
|
|
263
|
+
const picksResult = await pool.query(
|
|
264
|
+
`SELECT pp.*, pf.fighter_a_name, pf.fighter_b_name, pf.weight_class, pf.winner, pf.status as fight_status
|
|
265
|
+
FROM pickem_picks pp
|
|
266
|
+
JOIN pickem_fights pf ON pp.fight_id = pf.id
|
|
267
|
+
WHERE pp.entry_id = $1
|
|
268
|
+
ORDER BY pf.fight_order ASC`,
|
|
269
|
+
[entry.id]
|
|
270
|
+
);
|
|
271
|
+
entry.picks = picksResult.rows.map(formatPick);
|
|
272
|
+
|
|
273
|
+
return entry;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ========== PICKS ==========
|
|
277
|
+
|
|
278
|
+
async function submitPicks({ poolId, userId, picks }) {
|
|
279
|
+
const client = await pool.connect();
|
|
280
|
+
try {
|
|
281
|
+
await client.query('BEGIN');
|
|
282
|
+
|
|
283
|
+
// 1. Validate pool is open and not past lock_time
|
|
284
|
+
const poolResult = await client.query(
|
|
285
|
+
'SELECT * FROM pickem_pools WHERE id = $1',
|
|
286
|
+
[poolId]
|
|
287
|
+
);
|
|
288
|
+
if (poolResult.rows.length === 0) throw new Error('Pool not found');
|
|
289
|
+
const pickemPool = poolResult.rows[0];
|
|
290
|
+
if (pickemPool.status !== 'open') throw new Error(`Pool is ${pickemPool.status}, cannot submit picks`);
|
|
291
|
+
if (new Date() >= new Date(pickemPool.lock_time)) throw new Error('Lock time has passed, picks are closed');
|
|
292
|
+
|
|
293
|
+
// 2. Validate entry exists
|
|
294
|
+
const entryResult = await client.query(
|
|
295
|
+
'SELECT * FROM pickem_entries WHERE pool_id = $1 AND user_id = $2',
|
|
296
|
+
[poolId, userId]
|
|
297
|
+
);
|
|
298
|
+
if (entryResult.rows.length === 0) throw new Error('Must join pool before submitting picks');
|
|
299
|
+
const entry = entryResult.rows[0];
|
|
300
|
+
|
|
301
|
+
// 3. Validate all active fights are covered
|
|
302
|
+
const fightsResult = await client.query(
|
|
303
|
+
"SELECT id FROM pickem_fights WHERE pool_id = $1 AND status NOT IN ('cancelled', 'no_contest')",
|
|
304
|
+
[poolId]
|
|
305
|
+
);
|
|
306
|
+
const validFightIds = new Set(fightsResult.rows.map(r => r.id));
|
|
307
|
+
|
|
308
|
+
if (picks.length !== validFightIds.size) {
|
|
309
|
+
throw new Error(`Must pick all ${validFightIds.size} fights, received ${picks.length}`);
|
|
310
|
+
}
|
|
311
|
+
for (const p of picks) {
|
|
312
|
+
if (!validFightIds.has(p.fightId)) throw new Error(`Invalid fight ID: ${p.fightId}`);
|
|
313
|
+
if (p.pick !== 'a' && p.pick !== 'b') throw new Error(`Invalid pick "${p.pick}", must be "a" or "b"`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// 4. Upsert all picks
|
|
317
|
+
const insertedPicks = [];
|
|
318
|
+
for (const p of picks) {
|
|
319
|
+
const result = await client.query(
|
|
320
|
+
`INSERT INTO pickem_picks (entry_id, fight_id, pick)
|
|
321
|
+
VALUES ($1, $2, $3)
|
|
322
|
+
ON CONFLICT (entry_id, fight_id)
|
|
323
|
+
DO UPDATE SET pick = EXCLUDED.pick, updated_at = NOW()
|
|
324
|
+
RETURNING *`,
|
|
325
|
+
[entry.id, p.fightId, p.pick]
|
|
326
|
+
);
|
|
327
|
+
insertedPicks.push(result.rows[0]);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
await client.query('COMMIT');
|
|
331
|
+
return insertedPicks.map(formatPick);
|
|
332
|
+
} catch (error) {
|
|
333
|
+
await client.query('ROLLBACK');
|
|
334
|
+
throw error;
|
|
335
|
+
} finally {
|
|
336
|
+
client.release();
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function getUserPicks(poolId, userId) {
|
|
341
|
+
const entryResult = await pool.query(
|
|
342
|
+
'SELECT id FROM pickem_entries WHERE pool_id = $1 AND user_id = $2',
|
|
343
|
+
[poolId, userId]
|
|
344
|
+
);
|
|
345
|
+
if (entryResult.rows.length === 0) return [];
|
|
346
|
+
|
|
347
|
+
const result = await pool.query(
|
|
348
|
+
`SELECT pp.*, pf.fighter_a_name, pf.fighter_b_name, pf.weight_class, pf.winner, pf.status as fight_status
|
|
349
|
+
FROM pickem_picks pp
|
|
350
|
+
JOIN pickem_fights pf ON pp.fight_id = pf.id
|
|
351
|
+
WHERE pp.entry_id = $1
|
|
352
|
+
ORDER BY pf.fight_order ASC`,
|
|
353
|
+
[entryResult.rows[0].id]
|
|
354
|
+
);
|
|
355
|
+
return result.rows.map(formatPick);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// ========== SCORING ==========
|
|
359
|
+
|
|
360
|
+
async function computeScores(poolId) {
|
|
361
|
+
const client = await pool.connect();
|
|
362
|
+
try {
|
|
363
|
+
await client.query('BEGIN');
|
|
364
|
+
|
|
365
|
+
// 1. Mark picks correct/incorrect based on resolved fights
|
|
366
|
+
await client.query(
|
|
367
|
+
`UPDATE pickem_picks pp
|
|
368
|
+
SET is_correct = (pp.pick = pf.winner), updated_at = NOW()
|
|
369
|
+
FROM pickem_fights pf
|
|
370
|
+
WHERE pp.fight_id = pf.id
|
|
371
|
+
AND pf.pool_id = $1
|
|
372
|
+
AND pf.status = 'final'
|
|
373
|
+
AND pf.winner IS NOT NULL`,
|
|
374
|
+
[poolId]
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
// 2. Cancelled/no_contest fights: exclude from scoring
|
|
378
|
+
await client.query(
|
|
379
|
+
`UPDATE pickem_picks pp
|
|
380
|
+
SET is_correct = NULL, updated_at = NOW()
|
|
381
|
+
FROM pickem_fights pf
|
|
382
|
+
WHERE pp.fight_id = pf.id
|
|
383
|
+
AND pf.pool_id = $1
|
|
384
|
+
AND pf.status IN ('cancelled', 'no_contest')`,
|
|
385
|
+
[poolId]
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
// 3. Compute scores: count correct picks per entry
|
|
389
|
+
await client.query(
|
|
390
|
+
`UPDATE pickem_entries pe
|
|
391
|
+
SET score = (
|
|
392
|
+
SELECT COUNT(*) FROM pickem_picks pp WHERE pp.entry_id = pe.id AND pp.is_correct = true
|
|
393
|
+
),
|
|
394
|
+
updated_at = NOW()
|
|
395
|
+
WHERE pe.pool_id = $1`,
|
|
396
|
+
[poolId]
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
// 4. Compute ranks using DENSE_RANK
|
|
400
|
+
await client.query(
|
|
401
|
+
`UPDATE pickem_entries pe
|
|
402
|
+
SET rank = sub.rank
|
|
403
|
+
FROM (
|
|
404
|
+
SELECT id, DENSE_RANK() OVER (ORDER BY score DESC) as rank
|
|
405
|
+
FROM pickem_entries WHERE pool_id = $1
|
|
406
|
+
) sub
|
|
407
|
+
WHERE pe.id = sub.id`,
|
|
408
|
+
[poolId]
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
// 5. Get results
|
|
412
|
+
const result = await client.query(
|
|
413
|
+
'SELECT pe.*, u.username, u.avatar FROM pickem_entries pe LEFT JOIN users u ON pe.user_id = u.id WHERE pe.pool_id = $1 ORDER BY score DESC, created_at ASC',
|
|
414
|
+
[poolId]
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
const entries = result.rows.map(formatEntry);
|
|
418
|
+
const maxScore = entries.length > 0 ? entries[0].score : 0;
|
|
419
|
+
const winners = entries.filter(e => e.score === maxScore);
|
|
420
|
+
|
|
421
|
+
await client.query('COMMIT');
|
|
422
|
+
|
|
423
|
+
return { entries, maxScore, winnerCount: winners.length, winners };
|
|
424
|
+
} catch (error) {
|
|
425
|
+
await client.query('ROLLBACK');
|
|
426
|
+
throw error;
|
|
427
|
+
} finally {
|
|
428
|
+
client.release();
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// ========== LEADERBOARD ==========
|
|
433
|
+
|
|
434
|
+
async function getLeaderboard(poolId) {
|
|
435
|
+
const result = await pool.query(
|
|
436
|
+
`SELECT pe.*, u.username, u.avatar
|
|
437
|
+
FROM pickem_entries pe
|
|
438
|
+
LEFT JOIN users u ON pe.user_id = u.id
|
|
439
|
+
WHERE pe.pool_id = $1
|
|
440
|
+
ORDER BY pe.score DESC, pe.created_at ASC`,
|
|
441
|
+
[poolId]
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
const entries = result.rows.map(formatEntry);
|
|
445
|
+
const entryIds = entries.map(e => e.id);
|
|
446
|
+
|
|
447
|
+
if (entryIds.length > 0) {
|
|
448
|
+
const picksResult = await pool.query(
|
|
449
|
+
`SELECT pp.*, pf.fighter_a_name, pf.fighter_b_name, pf.weight_class,
|
|
450
|
+
pf.winner, pf.status as fight_status, pf.fight_order
|
|
451
|
+
FROM pickem_picks pp
|
|
452
|
+
JOIN pickem_fights pf ON pp.fight_id = pf.id
|
|
453
|
+
WHERE pp.entry_id = ANY($1)
|
|
454
|
+
ORDER BY pf.fight_order ASC`,
|
|
455
|
+
[entryIds]
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
const picksByEntry = {};
|
|
459
|
+
for (const row of picksResult.rows) {
|
|
460
|
+
if (!picksByEntry[row.entry_id]) picksByEntry[row.entry_id] = [];
|
|
461
|
+
picksByEntry[row.entry_id].push({
|
|
462
|
+
...formatPick(row),
|
|
463
|
+
fightOrder: row.fight_order,
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return entries.map(e => ({ ...e, picks: picksByEntry[e.id] || [] }));
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return entries;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
async function getWinners(poolId) {
|
|
474
|
+
const result = await pool.query(
|
|
475
|
+
`SELECT pe.*, u.username, u.avatar
|
|
476
|
+
FROM pickem_entries pe
|
|
477
|
+
LEFT JOIN users u ON pe.user_id = u.id
|
|
478
|
+
WHERE pe.pool_id = $1 AND pe.rank = 1`,
|
|
479
|
+
[poolId]
|
|
480
|
+
);
|
|
481
|
+
return result.rows.map(formatEntry);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// ========== STATS ==========
|
|
485
|
+
|
|
486
|
+
async function getPoolStats(poolId) {
|
|
487
|
+
const poolResult = await pool.query('SELECT * FROM pickem_pools WHERE id = $1', [poolId]);
|
|
488
|
+
if (poolResult.rows.length === 0) return null;
|
|
489
|
+
const p = poolResult.rows[0];
|
|
490
|
+
|
|
491
|
+
const fightsResult = await pool.query(
|
|
492
|
+
"SELECT COUNT(*) as total, COUNT(*) FILTER (WHERE status IN ('final', 'cancelled', 'no_contest')) as resolved FROM pickem_fights WHERE pool_id = $1",
|
|
493
|
+
[poolId]
|
|
494
|
+
);
|
|
495
|
+
const { total: totalFights, resolved: resolvedFights } = fightsResult.rows[0];
|
|
496
|
+
|
|
497
|
+
const totalEntries = Number(p.total_entries || 0);
|
|
498
|
+
const totalPotLamports = totalEntries * Number(p.buy_in_lamports);
|
|
499
|
+
|
|
500
|
+
return {
|
|
501
|
+
totalEntries,
|
|
502
|
+
totalPotLamports,
|
|
503
|
+
totalPotSol: totalPotLamports / LAMPORTS_PER_SOL,
|
|
504
|
+
netPotLamports: Math.floor(totalPotLamports * (1 - FEE_PERCENT / 100)),
|
|
505
|
+
netPotSol: Math.floor(totalPotLamports * (1 - FEE_PERCENT / 100)) / LAMPORTS_PER_SOL,
|
|
506
|
+
totalFights: Number(totalFights),
|
|
507
|
+
resolvedFights: Number(resolvedFights),
|
|
508
|
+
allFightsResolved: Number(resolvedFights) === Number(totalFights) && Number(totalFights) > 0,
|
|
509
|
+
status: p.status,
|
|
510
|
+
lockTime: p.lock_time,
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// ========== PAYOUTS ==========
|
|
515
|
+
|
|
516
|
+
async function recordPayout({ poolId, entryId, walletAddress, amountLamports, txSignature, status }) {
|
|
517
|
+
const result = await pool.query(
|
|
518
|
+
`INSERT INTO pickem_payouts (pool_id, entry_id, wallet_address, amount_lamports, payout_tx_signature, status)
|
|
519
|
+
VALUES ($1, $2, $3, $4, $5, $6)
|
|
520
|
+
ON CONFLICT (pool_id, entry_id)
|
|
521
|
+
DO UPDATE SET payout_tx_signature = EXCLUDED.payout_tx_signature, status = EXCLUDED.status, updated_at = NOW()
|
|
522
|
+
RETURNING *`,
|
|
523
|
+
[poolId, entryId, walletAddress, amountLamports, txSignature || null, status || 'pending']
|
|
524
|
+
);
|
|
525
|
+
return result.rows[0];
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
async function getPayouts(poolId) {
|
|
529
|
+
const result = await pool.query(
|
|
530
|
+
'SELECT * FROM pickem_payouts WHERE pool_id = $1 ORDER BY amount_lamports DESC',
|
|
531
|
+
[poolId]
|
|
532
|
+
);
|
|
533
|
+
return result.rows;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
module.exports = {
|
|
537
|
+
createPool,
|
|
538
|
+
getPools,
|
|
539
|
+
getPoolById,
|
|
540
|
+
updatePool,
|
|
541
|
+
importFights,
|
|
542
|
+
getFights,
|
|
543
|
+
updateFightResult,
|
|
544
|
+
joinPool,
|
|
545
|
+
getUserEntry,
|
|
546
|
+
submitPicks,
|
|
547
|
+
getUserPicks,
|
|
548
|
+
computeScores,
|
|
549
|
+
getLeaderboard,
|
|
550
|
+
getWinners,
|
|
551
|
+
getPoolStats,
|
|
552
|
+
recordPayout,
|
|
553
|
+
getPayouts,
|
|
554
|
+
};
|