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,440 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pick'em Oracle
|
|
3
|
+
* Polls UFC fight results via @dubsdotapp/node SDK, resolves pools, distributes winnings on-chain.
|
|
4
|
+
* Pattern mirrors survivorOracle.js
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { Dubs } = require('@dubsdotapp/node');
|
|
8
|
+
const { Connection, Keypair, PublicKey, Transaction, TransactionInstruction } = require('@solana/web3.js');
|
|
9
|
+
const crypto = require('crypto');
|
|
10
|
+
const { pool } = require('./db');
|
|
11
|
+
const pickemController = require('../controllers/pickemController');
|
|
12
|
+
|
|
13
|
+
const LAMPORTS_PER_SOL = 1_000_000_000;
|
|
14
|
+
const OPERATOR_WALLET = new PublicKey('BVZXwZpfgyzTBdRFHohkHZppPHnAyqyctRsKy3vWfQib');
|
|
15
|
+
|
|
16
|
+
// distribute_survivor_winnings discriminator (sha256("global:distribute_survivor_winnings")[0..8])
|
|
17
|
+
const DIST_SURVIVOR = Buffer.from(
|
|
18
|
+
crypto.createHash('sha256').update('global:distribute_survivor_winnings').digest().slice(0, 8)
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
class PickemOracle {
|
|
22
|
+
constructor(config = {}) {
|
|
23
|
+
this.checkIntervalMs = config.checkIntervalMs || 5 * 60 * 1000; // 5 minutes
|
|
24
|
+
this.isRunning = false;
|
|
25
|
+
this.intervalId = null;
|
|
26
|
+
|
|
27
|
+
// Solana config
|
|
28
|
+
this.rpcUrl = config.rpcUrl || process.env.SOLANA_RPC_URL || 'https://api.devnet.solana.com';
|
|
29
|
+
this.programId = new PublicKey(config.programId || process.env.PROGRAM_ID || '85wJGp9uc8w2FeKX9CEHsudTo1UVCrmuRFy37oCcaoG1');
|
|
30
|
+
this.connection = new Connection(this.rpcUrl, 'confirmed');
|
|
31
|
+
this.oracleKeypair = config.oracleKeypair || null;
|
|
32
|
+
|
|
33
|
+
// Dubs SDK client — dogfooding our own API
|
|
34
|
+
this.dubs = new Dubs({
|
|
35
|
+
apiKey: config.apiKey || process.env.DUBS_API_KEY || '',
|
|
36
|
+
baseUrl: config.baseUrl || process.env.DUBS_SDK_BASE_URL || `http://localhost:${process.env.PORT || 3001}/api/developer/v1`,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Socket.IO ref (injected externally if available)
|
|
40
|
+
this.io = config.io || null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
start() {
|
|
44
|
+
if (this.isRunning) return;
|
|
45
|
+
this.isRunning = true;
|
|
46
|
+
|
|
47
|
+
console.log('🥊 Starting Pick\'em Oracle...');
|
|
48
|
+
console.log(` Check interval: ${this.checkIntervalMs / 1000}s`);
|
|
49
|
+
console.log(` RPC: ${this.rpcUrl}`);
|
|
50
|
+
console.log(` Program: ${this.programId.toString()}`);
|
|
51
|
+
if (this.oracleKeypair) {
|
|
52
|
+
console.log(` Oracle wallet: ${this.oracleKeypair.publicKey.toString()}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Run immediately, then on interval
|
|
56
|
+
this.checkPools().catch(err => console.error('[PickemOracle] Initial check error:', err.message));
|
|
57
|
+
this.intervalId = setInterval(() => {
|
|
58
|
+
this.checkPools().catch(err => console.error('[PickemOracle] Check error:', err.message));
|
|
59
|
+
}, this.checkIntervalMs);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
stop() {
|
|
63
|
+
if (!this.isRunning) return;
|
|
64
|
+
this.isRunning = false;
|
|
65
|
+
if (this.intervalId) {
|
|
66
|
+
clearInterval(this.intervalId);
|
|
67
|
+
this.intervalId = null;
|
|
68
|
+
}
|
|
69
|
+
console.log('🥊 Pick\'em Oracle stopped.');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ========== MAIN POLLING LOOP ==========
|
|
73
|
+
|
|
74
|
+
async checkPools() {
|
|
75
|
+
console.log('[PickemOracle] Checking pools...');
|
|
76
|
+
|
|
77
|
+
// 1. Auto-lock pools past lock_time
|
|
78
|
+
await this.autoLockPools();
|
|
79
|
+
|
|
80
|
+
// 2. Get active pools (locked or resolving)
|
|
81
|
+
const result = await pool.query(
|
|
82
|
+
"SELECT * FROM pickem_pools WHERE status IN ('locked', 'resolving')"
|
|
83
|
+
);
|
|
84
|
+
const activePools = result.rows;
|
|
85
|
+
|
|
86
|
+
if (activePools.length === 0) {
|
|
87
|
+
console.log('[PickemOracle] No active pools to process.');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
console.log(`[PickemOracle] Processing ${activePools.length} active pool(s)...`);
|
|
92
|
+
|
|
93
|
+
// 3. Fetch fight card data via SDK
|
|
94
|
+
let fightCardData;
|
|
95
|
+
try {
|
|
96
|
+
fightCardData = await this.dubs.ufc.fightCard();
|
|
97
|
+
} catch (err) {
|
|
98
|
+
console.error('[PickemOracle] Failed to fetch UFC fight card via SDK:', err.message);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Build a lookup: competitionId → fight result
|
|
103
|
+
const fightResultMap = new Map();
|
|
104
|
+
if (fightCardData && fightCardData.events) {
|
|
105
|
+
for (const event of fightCardData.events) {
|
|
106
|
+
for (const fight of event.fights) {
|
|
107
|
+
if (fight.id) {
|
|
108
|
+
fightResultMap.set(fight.id, fight);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
console.log(`[PickemOracle] UFC fight data: ${fightResultMap.size} fights tracked`);
|
|
115
|
+
|
|
116
|
+
// 4. Process each pool
|
|
117
|
+
for (const p of activePools) {
|
|
118
|
+
try {
|
|
119
|
+
await this.processPool(p, fightResultMap);
|
|
120
|
+
} catch (err) {
|
|
121
|
+
console.error(`[PickemOracle] Error processing pool ${p.id}:`, err.message);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ========== AUTO-LOCK ==========
|
|
127
|
+
|
|
128
|
+
async autoLockPools() {
|
|
129
|
+
const result = await pool.query(
|
|
130
|
+
"UPDATE pickem_pools SET status = 'locked', updated_at = NOW() WHERE status = 'open' AND lock_time <= NOW() RETURNING id, name"
|
|
131
|
+
);
|
|
132
|
+
for (const row of result.rows) {
|
|
133
|
+
console.log(`[PickemOracle] Auto-locked pool ${row.id}: ${row.name}`);
|
|
134
|
+
if (this.io) {
|
|
135
|
+
this.io.emit('pickem:pool_locked', { poolId: row.id });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ========== PROCESS POOL ==========
|
|
141
|
+
|
|
142
|
+
async processPool(pickemPool, fightResultMap) {
|
|
143
|
+
// Get unresolved fights for this pool
|
|
144
|
+
const fightsResult = await pool.query(
|
|
145
|
+
"SELECT * FROM pickem_fights WHERE pool_id = $1 AND status NOT IN ('final', 'cancelled', 'no_contest') ORDER BY fight_order",
|
|
146
|
+
[pickemPool.id]
|
|
147
|
+
);
|
|
148
|
+
const unresolvedFights = fightsResult.rows;
|
|
149
|
+
|
|
150
|
+
if (unresolvedFights.length === 0) {
|
|
151
|
+
// All fights resolved — trigger full pool resolution
|
|
152
|
+
console.log(`[PickemOracle] Pool ${pickemPool.id}: All fights resolved, triggering pool resolution...`);
|
|
153
|
+
await this.resolvePool(pickemPool.id);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Update each unresolved fight from SDK data
|
|
158
|
+
let newlyResolved = 0;
|
|
159
|
+
for (const fight of unresolvedFights) {
|
|
160
|
+
const sdkFight = fightResultMap.get(fight.espn_competition_id);
|
|
161
|
+
if (!sdkFight) continue;
|
|
162
|
+
|
|
163
|
+
if (sdkFight.status === 'Final') {
|
|
164
|
+
// Determine winner: 'a' = home/first, 'b' = away/second
|
|
165
|
+
let winner = null;
|
|
166
|
+
if (sdkFight.home?.winner === true) {
|
|
167
|
+
winner = 'a';
|
|
168
|
+
} else if (sdkFight.away?.winner === true) {
|
|
169
|
+
winner = 'b';
|
|
170
|
+
} else if (sdkFight.home?.score > sdkFight.away?.score) {
|
|
171
|
+
winner = 'a';
|
|
172
|
+
} else if (sdkFight.away?.score > sdkFight.home?.score) {
|
|
173
|
+
winner = 'b';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const method = sdkFight.ufcData?.statusDetail || sdkFight.statusDetail || null;
|
|
177
|
+
|
|
178
|
+
if (!winner) {
|
|
179
|
+
console.log(`[PickemOracle] Pool ${pickemPool.id} Fight #${fight.fight_order}: ${fight.fighter_a_name} vs ${fight.fighter_b_name} → FINAL but no winner detected! SDK data: home.winner=${sdkFight.home?.winner}, away.winner=${sdkFight.away?.winner}, home.score=${sdkFight.home?.score}, away.score=${sdkFight.away?.score}`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
await pickemController.updateFightResult(fight.id, {
|
|
183
|
+
winner,
|
|
184
|
+
method,
|
|
185
|
+
status: 'final',
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const winnerName = winner === 'a' ? fight.fighter_a_name : winner === 'b' ? fight.fighter_b_name : 'UNKNOWN';
|
|
189
|
+
console.log(`[PickemOracle] Pool ${pickemPool.id} Fight #${fight.fight_order}: ${fight.fighter_a_name} vs ${fight.fighter_b_name} → Winner: ${winnerName} (${method || 'unknown'})`);
|
|
190
|
+
|
|
191
|
+
if (this.io) {
|
|
192
|
+
this.io.emit('pickem:fight_resolved', {
|
|
193
|
+
poolId: pickemPool.id,
|
|
194
|
+
fightId: fight.id,
|
|
195
|
+
winner,
|
|
196
|
+
method,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
newlyResolved++;
|
|
200
|
+
} else if (['Canceled', 'Postponed', 'Cancelled'].includes(sdkFight.status)) {
|
|
201
|
+
await pickemController.updateFightResult(fight.id, {
|
|
202
|
+
winner: null,
|
|
203
|
+
method: null,
|
|
204
|
+
status: 'cancelled',
|
|
205
|
+
});
|
|
206
|
+
console.log(`[PickemOracle] Pool ${pickemPool.id} Fight #${fight.fight_order}: ${fight.fighter_a_name} vs ${fight.fighter_b_name} → Cancelled`);
|
|
207
|
+
newlyResolved++;
|
|
208
|
+
} else if (sdkFight.status === 'In Progress') {
|
|
209
|
+
await pool.query(
|
|
210
|
+
"UPDATE pickem_fights SET status = 'live', updated_at = NOW() WHERE id = $1",
|
|
211
|
+
[fight.id]
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (newlyResolved > 0) {
|
|
217
|
+
console.log(`[PickemOracle] Pool ${pickemPool.id}: ${newlyResolved} fight(s) newly resolved`);
|
|
218
|
+
|
|
219
|
+
// Incrementally mark picks correct/incorrect and recompute scores
|
|
220
|
+
try {
|
|
221
|
+
// Mark is_correct for all resolved fights in this pool
|
|
222
|
+
await pool.query(`
|
|
223
|
+
UPDATE pickem_picks pp
|
|
224
|
+
SET is_correct = (pp.pick = f.winner), updated_at = NOW()
|
|
225
|
+
FROM pickem_fights f
|
|
226
|
+
WHERE pp.fight_id = f.id
|
|
227
|
+
AND f.pool_id = $1
|
|
228
|
+
AND f.status = 'final'
|
|
229
|
+
AND f.winner IS NOT NULL
|
|
230
|
+
AND pp.is_correct IS NULL
|
|
231
|
+
`, [pickemPool.id]);
|
|
232
|
+
|
|
233
|
+
// Recompute scores for all entries in this pool
|
|
234
|
+
await pool.query(`
|
|
235
|
+
UPDATE pickem_entries e
|
|
236
|
+
SET score = (SELECT COUNT(*) FROM pickem_picks p WHERE p.entry_id = e.id AND p.is_correct = true),
|
|
237
|
+
updated_at = NOW()
|
|
238
|
+
WHERE e.pool_id = $1
|
|
239
|
+
`, [pickemPool.id]);
|
|
240
|
+
|
|
241
|
+
console.log(`[PickemOracle] Pool ${pickemPool.id}: Scores updated incrementally`);
|
|
242
|
+
} catch (scoreErr) {
|
|
243
|
+
console.error(`[PickemOracle] Pool ${pickemPool.id}: Error updating incremental scores:`, scoreErr.message);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Check if all fights are now terminal
|
|
247
|
+
const stats = await pickemController.getPoolStats(pickemPool.id);
|
|
248
|
+
if (stats.allFightsResolved) {
|
|
249
|
+
console.log(`[PickemOracle] Pool ${pickemPool.id}: All fights now resolved, triggering pool resolution...`);
|
|
250
|
+
await this.resolvePool(pickemPool.id);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ========== RESOLVE POOL ==========
|
|
256
|
+
|
|
257
|
+
async resolvePool(poolId) {
|
|
258
|
+
const client = await pool.connect();
|
|
259
|
+
try {
|
|
260
|
+
await client.query('BEGIN');
|
|
261
|
+
|
|
262
|
+
// Lock pool row
|
|
263
|
+
const poolResult = await client.query(
|
|
264
|
+
"SELECT * FROM pickem_pools WHERE id = $1 FOR UPDATE",
|
|
265
|
+
[poolId]
|
|
266
|
+
);
|
|
267
|
+
const pickemPool = poolResult.rows[0];
|
|
268
|
+
if (!pickemPool) throw new Error(`Pool ${poolId} not found`);
|
|
269
|
+
if (pickemPool.status === 'complete') {
|
|
270
|
+
console.log(`[PickemOracle] Pool ${poolId} already complete, skipping.`);
|
|
271
|
+
await client.query('COMMIT');
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Set status to resolving
|
|
276
|
+
await client.query(
|
|
277
|
+
"UPDATE pickem_pools SET status = 'resolving', updated_at = NOW() WHERE id = $1",
|
|
278
|
+
[poolId]
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
await client.query('COMMIT');
|
|
282
|
+
} catch (err) {
|
|
283
|
+
await client.query('ROLLBACK');
|
|
284
|
+
throw err;
|
|
285
|
+
} finally {
|
|
286
|
+
client.release();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Compute scores (uses its own transaction)
|
|
290
|
+
console.log(`[PickemOracle] Pool ${poolId}: Computing scores...`);
|
|
291
|
+
const scoreResult = await pickemController.computeScores(poolId);
|
|
292
|
+
console.log(`[PickemOracle] Pool ${poolId}: Max score = ${scoreResult.maxScore}, Winners = ${scoreResult.winnerCount}`);
|
|
293
|
+
|
|
294
|
+
// Distribute on-chain if we have a Solana game
|
|
295
|
+
const poolResult = await pool.query('SELECT * FROM pickem_pools WHERE id = $1', [poolId]);
|
|
296
|
+
const pickemPool = poolResult.rows[0];
|
|
297
|
+
|
|
298
|
+
if (pickemPool.solana_game_id && this.oracleKeypair && scoreResult.winners.length > 0) {
|
|
299
|
+
try {
|
|
300
|
+
console.log(`[PickemOracle] Pool ${poolId}: Distributing winnings on-chain...`);
|
|
301
|
+
const signatures = await this.distributeWinnings(pickemPool, scoreResult.winners);
|
|
302
|
+
|
|
303
|
+
// Record payouts
|
|
304
|
+
const totalEntries = Number(pickemPool.total_entries);
|
|
305
|
+
const totalPotLamports = totalEntries * Number(pickemPool.buy_in_lamports);
|
|
306
|
+
const netPotLamports = Math.floor(totalPotLamports * 0.94); // 6% fee
|
|
307
|
+
const perWinner = Math.floor(netPotLamports / scoreResult.winnerCount);
|
|
308
|
+
|
|
309
|
+
for (const winner of scoreResult.winners) {
|
|
310
|
+
await pickemController.recordPayout({
|
|
311
|
+
poolId,
|
|
312
|
+
entryId: winner.id,
|
|
313
|
+
walletAddress: winner.walletAddress,
|
|
314
|
+
amountLamports: perWinner,
|
|
315
|
+
txSignature: signatures[0], // First batch signature
|
|
316
|
+
status: 'completed',
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
console.log(`[PickemOracle] Pool ${poolId}: Payouts recorded. ${scoreResult.winnerCount} winner(s), ${perWinner / LAMPORTS_PER_SOL} SOL each.`);
|
|
321
|
+
} catch (err) {
|
|
322
|
+
console.error(`[PickemOracle] Pool ${poolId}: On-chain distribution failed:`, err.message);
|
|
323
|
+
// Mark payouts as failed but still complete the pool
|
|
324
|
+
for (const winner of scoreResult.winners) {
|
|
325
|
+
await pickemController.recordPayout({
|
|
326
|
+
poolId,
|
|
327
|
+
entryId: winner.id,
|
|
328
|
+
walletAddress: winner.walletAddress,
|
|
329
|
+
amountLamports: 0,
|
|
330
|
+
txSignature: null,
|
|
331
|
+
status: 'failed',
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
} else {
|
|
336
|
+
if (!pickemPool.solana_game_id) console.log(`[PickemOracle] Pool ${poolId}: No solana_game_id, skipping on-chain distribution.`);
|
|
337
|
+
if (!this.oracleKeypair) console.log(`[PickemOracle] Pool ${poolId}: No oracle keypair, skipping on-chain distribution.`);
|
|
338
|
+
if (scoreResult.winners.length === 0) console.log(`[PickemOracle] Pool ${poolId}: No entries, skipping distribution.`);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Mark pool complete
|
|
342
|
+
await pool.query(
|
|
343
|
+
"UPDATE pickem_pools SET status = 'complete', updated_at = NOW() WHERE id = $1",
|
|
344
|
+
[poolId]
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
console.log(`[PickemOracle] Pool ${poolId}: COMPLETE ✅`);
|
|
348
|
+
|
|
349
|
+
if (this.io) {
|
|
350
|
+
this.io.emit('pickem:pool_resolved', {
|
|
351
|
+
poolId,
|
|
352
|
+
maxScore: scoreResult.maxScore,
|
|
353
|
+
winnerCount: scoreResult.winnerCount,
|
|
354
|
+
winners: scoreResult.winners.map(w => ({
|
|
355
|
+
walletAddress: w.walletAddress,
|
|
356
|
+
username: w.username,
|
|
357
|
+
score: w.score,
|
|
358
|
+
})),
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ========== SOLANA DISTRIBUTION ==========
|
|
364
|
+
|
|
365
|
+
async distributeWinnings(pickemPool, winners) {
|
|
366
|
+
const gameIdNum = BigInt(pickemPool.solana_game_id);
|
|
367
|
+
const gameIdBuf = Buffer.alloc(8);
|
|
368
|
+
gameIdBuf.writeBigUInt64LE(gameIdNum);
|
|
369
|
+
|
|
370
|
+
const [gamePDA] = PublicKey.findProgramAddressSync(
|
|
371
|
+
[Buffer.from('game'), gameIdBuf],
|
|
372
|
+
this.programId
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
// Batch winners (max 50 per tx due to contract limit)
|
|
376
|
+
const batches = [];
|
|
377
|
+
for (let i = 0; i < winners.length; i += 50) {
|
|
378
|
+
batches.push(winners.slice(i, i + 50));
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const signatures = [];
|
|
382
|
+
for (let batchIdx = 0; batchIdx < batches.length; batchIdx++) {
|
|
383
|
+
const batch = batches[batchIdx];
|
|
384
|
+
|
|
385
|
+
const data = Buffer.concat([DIST_SURVIVOR, gameIdBuf]);
|
|
386
|
+
|
|
387
|
+
const keys = [
|
|
388
|
+
{ pubkey: gamePDA, isSigner: false, isWritable: true },
|
|
389
|
+
{ pubkey: this.oracleKeypair.publicKey, isSigner: true, isWritable: true },
|
|
390
|
+
// remaining_accounts[0] = operator wallet
|
|
391
|
+
{ pubkey: OPERATOR_WALLET, isSigner: false, isWritable: true },
|
|
392
|
+
];
|
|
393
|
+
|
|
394
|
+
// remaining_accounts[1..n] = winner wallets
|
|
395
|
+
for (const w of batch) {
|
|
396
|
+
keys.push({ pubkey: new PublicKey(w.walletAddress), isSigner: false, isWritable: true });
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const ix = new TransactionInstruction({
|
|
400
|
+
keys,
|
|
401
|
+
programId: this.programId,
|
|
402
|
+
data,
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
const tx = new Transaction().add(ix);
|
|
406
|
+
const { blockhash } = await this.connection.getLatestBlockhash();
|
|
407
|
+
tx.recentBlockhash = blockhash;
|
|
408
|
+
tx.feePayer = this.oracleKeypair.publicKey;
|
|
409
|
+
tx.sign(this.oracleKeypair);
|
|
410
|
+
|
|
411
|
+
console.log(`[PickemOracle] Sending distribute_survivor_winnings batch ${batchIdx + 1}/${batches.length} (${batch.length} winners)...`);
|
|
412
|
+
const sig = await this.connection.sendRawTransaction(tx.serialize());
|
|
413
|
+
await this.confirmTransactionPolling(sig);
|
|
414
|
+
signatures.push(sig);
|
|
415
|
+
console.log(`[PickemOracle] Batch ${batchIdx + 1} confirmed: ${sig}`);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return signatures;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// ========== HELPERS ==========
|
|
422
|
+
|
|
423
|
+
async confirmTransactionPolling(signature, timeout = 60000) {
|
|
424
|
+
const start = Date.now();
|
|
425
|
+
while (Date.now() - start < timeout) {
|
|
426
|
+
const statuses = await this.connection.getSignatureStatuses([signature]);
|
|
427
|
+
const status = statuses?.value?.[0];
|
|
428
|
+
if (status?.err) {
|
|
429
|
+
throw new Error(`Transaction failed: ${JSON.stringify(status.err)}`);
|
|
430
|
+
}
|
|
431
|
+
if (status?.confirmationStatus === 'confirmed' || status?.confirmationStatus === 'finalized') {
|
|
432
|
+
return status;
|
|
433
|
+
}
|
|
434
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
435
|
+
}
|
|
436
|
+
throw new Error('Transaction confirmation timeout');
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
module.exports = PickemOracle;
|