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,1185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 📊 User Profile Stats Service
|
|
3
|
+
*
|
|
4
|
+
* High-performance service for fetching comprehensive user statistics
|
|
5
|
+
* including PNL from jackpot games AND sports betting games.
|
|
6
|
+
*
|
|
7
|
+
* Optimized with:
|
|
8
|
+
* - Single efficient SQL query joining all relevant tables
|
|
9
|
+
* - In-memory caching with TTL
|
|
10
|
+
* - Batch-friendly design for leaderboards
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { pool } = require('./db'); // Shared database pool
|
|
14
|
+
|
|
15
|
+
class UserProfileStatsService {
|
|
16
|
+
constructor() {
|
|
17
|
+
// Use shared pool from services/db.js
|
|
18
|
+
this.pool = pool;
|
|
19
|
+
|
|
20
|
+
// Simple in-memory cache with TTL (30 seconds)
|
|
21
|
+
this.cache = new Map();
|
|
22
|
+
this.CACHE_TTL = 30 * 1000; // 30 seconds
|
|
23
|
+
|
|
24
|
+
this.initializeTables();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async initializeTables() {
|
|
28
|
+
try {
|
|
29
|
+
// Create sports_betting_stats table for aggregated sports stats
|
|
30
|
+
await this.pool.query(`
|
|
31
|
+
CREATE TABLE IF NOT EXISTS sports_betting_stats (
|
|
32
|
+
wallet_address VARCHAR(100) PRIMARY KEY,
|
|
33
|
+
games_created INTEGER DEFAULT 0,
|
|
34
|
+
games_joined INTEGER DEFAULT 0,
|
|
35
|
+
games_won INTEGER DEFAULT 0,
|
|
36
|
+
games_lost INTEGER DEFAULT 0,
|
|
37
|
+
total_wagered_sol NUMERIC(20, 9) DEFAULT 0,
|
|
38
|
+
total_won_sol NUMERIC(20, 9) DEFAULT 0,
|
|
39
|
+
net_pnl_sol NUMERIC(20, 9) DEFAULT 0,
|
|
40
|
+
biggest_win_sol NUMERIC(20, 9) DEFAULT 0,
|
|
41
|
+
biggest_win_game_id VARCHAR(255),
|
|
42
|
+
current_win_streak INTEGER DEFAULT 0,
|
|
43
|
+
longest_win_streak INTEGER DEFAULT 0,
|
|
44
|
+
last_played_at TIMESTAMP,
|
|
45
|
+
created_at TIMESTAMP DEFAULT NOW(),
|
|
46
|
+
updated_at TIMESTAMP DEFAULT NOW()
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
CREATE INDEX IF NOT EXISTS idx_sports_stats_pnl ON sports_betting_stats(net_pnl_sol DESC);
|
|
50
|
+
CREATE INDEX IF NOT EXISTS idx_sports_stats_wagered ON sports_betting_stats(total_wagered_sol DESC);
|
|
51
|
+
CREATE INDEX IF NOT EXISTS idx_sports_stats_wins ON sports_betting_stats(games_won DESC);
|
|
52
|
+
`);
|
|
53
|
+
|
|
54
|
+
console.log('✅ User profile stats tables initialized');
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error('❌ Failed to initialize user profile stats tables:', error.message);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get comprehensive user profile stats (cached)
|
|
62
|
+
* @param {string} walletAddress - The user's wallet address
|
|
63
|
+
* @returns {Object} Complete stats object
|
|
64
|
+
*/
|
|
65
|
+
async getUserProfileStats(walletAddress) {
|
|
66
|
+
// Check cache first
|
|
67
|
+
const cached = this.cache.get(walletAddress);
|
|
68
|
+
if (cached && Date.now() - cached.timestamp < this.CACHE_TTL) {
|
|
69
|
+
return cached.data;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const stats = await this._fetchUserStats(walletAddress);
|
|
74
|
+
|
|
75
|
+
// Cache the result
|
|
76
|
+
this.cache.set(walletAddress, {
|
|
77
|
+
data: stats,
|
|
78
|
+
timestamp: Date.now()
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return stats;
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error('Error fetching user profile stats:', error);
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Internal method to fetch all stats with optimized queries
|
|
90
|
+
*/
|
|
91
|
+
async _fetchUserStats(walletAddress) {
|
|
92
|
+
const startTime = Date.now();
|
|
93
|
+
|
|
94
|
+
// Run queries in parallel for maximum performance
|
|
95
|
+
const [userInfo, jackpotStats, sportsStats, billiardsStats, connect4Stats, recentGames, dominantLeagues] = await Promise.all([
|
|
96
|
+
this._getUserInfo(walletAddress),
|
|
97
|
+
this._getJackpotStats(walletAddress),
|
|
98
|
+
this._getSportsStats(walletAddress),
|
|
99
|
+
this._getBilliardsStats(walletAddress),
|
|
100
|
+
this._getConnect4Stats(walletAddress),
|
|
101
|
+
this._getRecentGames(walletAddress, 10),
|
|
102
|
+
this._getDominantLeagues(walletAddress),
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
// Calculate combined stats (now includes billiards and connect4!)
|
|
106
|
+
const totalWagered = (jackpotStats?.totalWagered || 0) + (sportsStats?.totalWagered || 0) + (billiardsStats?.totalWagered || 0) + (connect4Stats?.totalWagered || 0);
|
|
107
|
+
const totalWon = (jackpotStats?.totalWon || 0) + (sportsStats?.totalWon || 0) + (billiardsStats?.totalWon || 0) + (connect4Stats?.totalWon || 0);
|
|
108
|
+
const netPNL = totalWon - totalWagered;
|
|
109
|
+
const totalGamesPlayed = (jackpotStats?.roundsPlayed || 0) + (sportsStats?.gamesPlayed || 0) + (billiardsStats?.roomsPlayed || 0) + (connect4Stats?.gamesPlayed || 0);
|
|
110
|
+
const totalGamesWon = (jackpotStats?.roundsWon || 0) + (sportsStats?.gamesWon || 0) + (billiardsStats?.roomsWon || 0) + (connect4Stats?.gamesWon || 0);
|
|
111
|
+
const winRate = totalGamesPlayed > 0 ? (totalGamesWon / totalGamesPlayed * 100) : 0;
|
|
112
|
+
|
|
113
|
+
const elapsed = Date.now() - startTime;
|
|
114
|
+
console.log(`📊 Fetched profile stats for ${walletAddress.slice(0, 8)}... in ${elapsed}ms (jackpot: ${!!jackpotStats}, sports: ${!!sportsStats}, billiards: ${!!billiardsStats}, connect4: ${!!connect4Stats})`);
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
// User Info
|
|
118
|
+
walletAddress,
|
|
119
|
+
username: userInfo?.username || null,
|
|
120
|
+
avatar: userInfo?.avatar || null,
|
|
121
|
+
preferredCurrency: userInfo?.preferred_currency || 'USD',
|
|
122
|
+
userId: userInfo?.id || null,
|
|
123
|
+
memberSince: userInfo?.created_at || null,
|
|
124
|
+
telegramConnected: !!userInfo?.telegram_user_id, // Whether user has Telegram linked
|
|
125
|
+
|
|
126
|
+
// Combined Stats
|
|
127
|
+
summary: {
|
|
128
|
+
totalWagered: parseFloat(totalWagered.toFixed(4)),
|
|
129
|
+
totalWon: parseFloat(totalWon.toFixed(4)),
|
|
130
|
+
netPNL: parseFloat(netPNL.toFixed(4)),
|
|
131
|
+
totalGamesPlayed,
|
|
132
|
+
totalGamesWon,
|
|
133
|
+
winRate: parseFloat(winRate.toFixed(2)),
|
|
134
|
+
isProfitable: netPNL > 0,
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
// Jackpot Stats (Solpot-style games)
|
|
138
|
+
jackpot: jackpotStats ? {
|
|
139
|
+
roundsPlayed: jackpotStats.roundsPlayed,
|
|
140
|
+
roundsWon: jackpotStats.roundsWon,
|
|
141
|
+
totalWagered: parseFloat((jackpotStats.totalWagered || 0).toFixed(4)),
|
|
142
|
+
totalWon: parseFloat((jackpotStats.totalWon || 0).toFixed(4)),
|
|
143
|
+
netPNL: parseFloat((jackpotStats.netPNL || 0).toFixed(4)),
|
|
144
|
+
biggestWin: parseFloat((jackpotStats.biggestWin || 0).toFixed(4)),
|
|
145
|
+
biggestWinRound: jackpotStats.biggestWinRound,
|
|
146
|
+
winRate: parseFloat(((jackpotStats.roundsPlayed > 0 ? jackpotStats.roundsWon / jackpotStats.roundsPlayed : 0) * 100).toFixed(2)),
|
|
147
|
+
} : null,
|
|
148
|
+
|
|
149
|
+
// Sports Betting Stats
|
|
150
|
+
sports: sportsStats ? {
|
|
151
|
+
gamesCreated: sportsStats.gamesCreated,
|
|
152
|
+
gamesJoined: sportsStats.gamesJoined,
|
|
153
|
+
gamesPlayed: sportsStats.gamesPlayed,
|
|
154
|
+
gamesWon: sportsStats.gamesWon,
|
|
155
|
+
gamesLost: sportsStats.gamesLost,
|
|
156
|
+
gamesPending: sportsStats.gamesPending,
|
|
157
|
+
totalWagered: parseFloat((sportsStats.totalWagered || 0).toFixed(4)),
|
|
158
|
+
pendingWagered: parseFloat((sportsStats.pendingWagered || 0).toFixed(4)),
|
|
159
|
+
totalWon: parseFloat((sportsStats.totalWon || 0).toFixed(4)),
|
|
160
|
+
netPNL: parseFloat((sportsStats.netPNL || 0).toFixed(4)),
|
|
161
|
+
biggestWin: parseFloat((sportsStats.biggestWin || 0).toFixed(4)),
|
|
162
|
+
currentStreak: sportsStats.currentStreak,
|
|
163
|
+
longestStreak: sportsStats.longestStreak,
|
|
164
|
+
winRate: parseFloat(((sportsStats.gamesPlayed > 0 ? sportsStats.gamesWon / sportsStats.gamesPlayed : 0) * 100).toFixed(2)),
|
|
165
|
+
} : null,
|
|
166
|
+
|
|
167
|
+
// 🎱 Billiards/Pool Room Stats
|
|
168
|
+
billiards: billiardsStats ? {
|
|
169
|
+
roomsCreated: billiardsStats.roomsCreated,
|
|
170
|
+
roomsJoined: billiardsStats.roomsJoined,
|
|
171
|
+
roomsPlayed: billiardsStats.roomsPlayed,
|
|
172
|
+
roomsWon: billiardsStats.roomsWon,
|
|
173
|
+
roomsLost: billiardsStats.roomsLost,
|
|
174
|
+
roomsPending: billiardsStats.roomsPending,
|
|
175
|
+
totalWagered: parseFloat((billiardsStats.totalWagered || 0).toFixed(4)),
|
|
176
|
+
pendingWagered: parseFloat((billiardsStats.pendingWagered || 0).toFixed(4)),
|
|
177
|
+
totalWon: parseFloat((billiardsStats.totalWon || 0).toFixed(4)),
|
|
178
|
+
netPNL: parseFloat((billiardsStats.netPNL || 0).toFixed(4)),
|
|
179
|
+
biggestWin: parseFloat((billiardsStats.biggestWin || 0).toFixed(4)),
|
|
180
|
+
biggestWinRoomId: billiardsStats.biggestWinRoomId,
|
|
181
|
+
currentStreak: billiardsStats.currentStreak,
|
|
182
|
+
longestStreak: billiardsStats.longestStreak,
|
|
183
|
+
winRate: parseFloat((billiardsStats.winRate || 0).toFixed(2)),
|
|
184
|
+
} : null,
|
|
185
|
+
|
|
186
|
+
// 🔴 Connect4 Stats
|
|
187
|
+
connect4: connect4Stats ? {
|
|
188
|
+
gamesCreated: connect4Stats.gamesCreated,
|
|
189
|
+
gamesJoined: connect4Stats.gamesJoined,
|
|
190
|
+
gamesPlayed: connect4Stats.gamesPlayed,
|
|
191
|
+
gamesWon: connect4Stats.gamesWon,
|
|
192
|
+
gamesLost: connect4Stats.gamesLost,
|
|
193
|
+
gamesPending: connect4Stats.gamesPending,
|
|
194
|
+
totalWagered: parseFloat((connect4Stats.totalWagered || 0).toFixed(4)),
|
|
195
|
+
pendingWagered: parseFloat((connect4Stats.pendingWagered || 0).toFixed(4)),
|
|
196
|
+
totalWon: parseFloat((connect4Stats.totalWon || 0).toFixed(4)),
|
|
197
|
+
netPNL: parseFloat((connect4Stats.netPNL || 0).toFixed(4)),
|
|
198
|
+
biggestWin: parseFloat((connect4Stats.biggestWin || 0).toFixed(4)),
|
|
199
|
+
biggestWinGameId: connect4Stats.biggestWinGameId,
|
|
200
|
+
currentStreak: connect4Stats.currentStreak,
|
|
201
|
+
longestStreak: connect4Stats.longestStreak,
|
|
202
|
+
winRate: parseFloat((connect4Stats.winRate || 0).toFixed(2)),
|
|
203
|
+
} : null,
|
|
204
|
+
|
|
205
|
+
// Recent Games (for history display)
|
|
206
|
+
recentGames,
|
|
207
|
+
|
|
208
|
+
// Badges based on dominant leagues/games
|
|
209
|
+
badges: dominantLeagues,
|
|
210
|
+
|
|
211
|
+
// Metadata
|
|
212
|
+
_meta: {
|
|
213
|
+
fetchedAt: new Date().toISOString(),
|
|
214
|
+
queryTimeMs: elapsed,
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Get basic user info
|
|
221
|
+
*/
|
|
222
|
+
async _getUserInfo(walletAddress) {
|
|
223
|
+
const result = await this.pool.query(
|
|
224
|
+
'SELECT id, wallet_address, username, avatar, preferred_currency, created_at, telegram_user_id FROM users WHERE wallet_address = $1',
|
|
225
|
+
[walletAddress]
|
|
226
|
+
);
|
|
227
|
+
return result.rows[0] || null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Get jackpot/solpot game stats
|
|
232
|
+
*/
|
|
233
|
+
async _getJackpotStats(walletAddress) {
|
|
234
|
+
const result = await this.pool.query(
|
|
235
|
+
'SELECT * FROM player_stats WHERE wallet_address = $1',
|
|
236
|
+
[walletAddress]
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
if (result.rows.length === 0) return null;
|
|
240
|
+
|
|
241
|
+
const stats = result.rows[0];
|
|
242
|
+
return {
|
|
243
|
+
totalWagered: parseFloat(stats.total_wagered || 0),
|
|
244
|
+
totalWon: parseFloat(stats.total_won || 0),
|
|
245
|
+
netPNL: parseFloat(stats.net_pnl || 0),
|
|
246
|
+
roundsPlayed: stats.rounds_played || 0,
|
|
247
|
+
roundsWon: stats.rounds_won || 0,
|
|
248
|
+
biggestWin: parseFloat(stats.biggest_win || 0),
|
|
249
|
+
biggestWinRound: stats.biggest_win_round,
|
|
250
|
+
lastPlayed: stats.last_played,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Get user's dominant leagues/games for badge display
|
|
256
|
+
* Includes sports leagues, connect4, and billiards
|
|
257
|
+
*/
|
|
258
|
+
async _getDominantLeagues(walletAddress) {
|
|
259
|
+
try {
|
|
260
|
+
// Get sports league counts
|
|
261
|
+
const sportsResult = await this.pool.query(`
|
|
262
|
+
SELECT
|
|
263
|
+
g.sports_event->>'strLeague' as league,
|
|
264
|
+
COUNT(*) as game_count
|
|
265
|
+
FROM user_game_refs ugr
|
|
266
|
+
JOIN games g ON ugr.game_id = g.game_id
|
|
267
|
+
WHERE ugr.wallet_address = $1
|
|
268
|
+
AND g.game_type = 'sports'
|
|
269
|
+
AND g.sports_event->>'strLeague' IS NOT NULL
|
|
270
|
+
GROUP BY g.sports_event->>'strLeague'
|
|
271
|
+
ORDER BY game_count DESC
|
|
272
|
+
LIMIT 5
|
|
273
|
+
`, [walletAddress]);
|
|
274
|
+
|
|
275
|
+
// Get connect4 game count
|
|
276
|
+
const connect4Result = await this.pool.query(`
|
|
277
|
+
SELECT COUNT(*) as game_count
|
|
278
|
+
FROM user_game_refs ugr
|
|
279
|
+
JOIN games g ON ugr.game_id = g.game_id
|
|
280
|
+
WHERE ugr.wallet_address = $1
|
|
281
|
+
AND g.game_type = 'connect4'
|
|
282
|
+
`, [walletAddress]);
|
|
283
|
+
|
|
284
|
+
// Get billiards game count
|
|
285
|
+
const billiardsResult = await this.pool.query(`
|
|
286
|
+
SELECT COUNT(*) as game_count
|
|
287
|
+
FROM user_game_refs ugr
|
|
288
|
+
JOIN games g ON ugr.game_id = g.game_id
|
|
289
|
+
WHERE ugr.wallet_address = $1
|
|
290
|
+
AND g.game_type = 'billiards'
|
|
291
|
+
`, [walletAddress]);
|
|
292
|
+
|
|
293
|
+
// Map leagues to emojis and badges
|
|
294
|
+
const leagueEmojis = {
|
|
295
|
+
'NFL': { emoji: '🏈', name: 'Football Fan' },
|
|
296
|
+
'NBA': { emoji: '🏀', name: 'Hoops Lover' },
|
|
297
|
+
'NHL': { emoji: '🏒', name: 'Hockey Head' },
|
|
298
|
+
'MLB': { emoji: '⚾', name: 'Baseball Boss' },
|
|
299
|
+
'English Premier League': { emoji: '⚽', name: 'Soccer Star' },
|
|
300
|
+
'La Liga': { emoji: '⚽', name: 'La Liga Legend' },
|
|
301
|
+
'Bundesliga': { emoji: '⚽', name: 'Bundesliga Beast' },
|
|
302
|
+
'Serie A': { emoji: '⚽', name: 'Serie A Shark' },
|
|
303
|
+
'Ligue 1': { emoji: '⚽', name: 'Ligue 1 Lion' },
|
|
304
|
+
'MLS': { emoji: '⚽', name: 'MLS Maven' },
|
|
305
|
+
'UFC': { emoji: '🥊', name: 'Fight Fan' },
|
|
306
|
+
'Boxing': { emoji: '🥊', name: 'Boxing Baron' },
|
|
307
|
+
'Tennis': { emoji: '🎾', name: 'Tennis Titan' },
|
|
308
|
+
'Golf': { emoji: '⛳', name: 'Golf Guru' },
|
|
309
|
+
'Formula 1': { emoji: '🏎️', name: 'F1 Fanatic' },
|
|
310
|
+
'NASCAR': { emoji: '🏁', name: 'NASCAR Nut' },
|
|
311
|
+
'connect4': { emoji: '🔴', name: 'Connect4 Pro' },
|
|
312
|
+
'billiards': { emoji: '🎱', name: 'Pool Shark' },
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const badges = sportsResult.rows.map(row => {
|
|
316
|
+
const leagueInfo = leagueEmojis[row.league] || { emoji: '🎮', name: row.league || 'Gamer' };
|
|
317
|
+
return {
|
|
318
|
+
league: row.league,
|
|
319
|
+
emoji: leagueInfo.emoji,
|
|
320
|
+
name: leagueInfo.name,
|
|
321
|
+
gamesPlayed: parseInt(row.game_count),
|
|
322
|
+
};
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// Add Connect4 badge if user has played
|
|
326
|
+
const connect4Count = parseInt(connect4Result.rows[0]?.game_count || 0);
|
|
327
|
+
if (connect4Count > 0) {
|
|
328
|
+
badges.push({
|
|
329
|
+
league: 'connect4',
|
|
330
|
+
emoji: leagueEmojis.connect4.emoji,
|
|
331
|
+
name: leagueEmojis.connect4.name,
|
|
332
|
+
gamesPlayed: connect4Count,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Add Billiards badge if user has played
|
|
337
|
+
const billiardsCount = parseInt(billiardsResult.rows[0]?.game_count || 0);
|
|
338
|
+
if (billiardsCount > 0) {
|
|
339
|
+
badges.push({
|
|
340
|
+
league: 'billiards',
|
|
341
|
+
emoji: leagueEmojis.billiards.emoji,
|
|
342
|
+
name: leagueEmojis.billiards.name,
|
|
343
|
+
gamesPlayed: billiardsCount,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Sort by games played and limit to top 5
|
|
348
|
+
return badges.sort((a, b) => b.gamesPlayed - a.gamesPlayed).slice(0, 5);
|
|
349
|
+
} catch (error) {
|
|
350
|
+
console.error('Error getting dominant leagues:', error);
|
|
351
|
+
return [];
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* 🎱 Get billiards/pool room stats
|
|
357
|
+
*
|
|
358
|
+
* Billiards uses game_mode = 0 (Manual mode)
|
|
359
|
+
* Winners are identified by status = 'won' in user_game_refs
|
|
360
|
+
* PNL is calculated from amount_claimed - buy_in
|
|
361
|
+
*/
|
|
362
|
+
async _getBilliardsStats(walletAddress) {
|
|
363
|
+
try {
|
|
364
|
+
// Get all billiards games the user participated in
|
|
365
|
+
// Note: winner info is stored in sports_event->'winner'->>'walletAddress' (from billiards settlement)
|
|
366
|
+
const result = await this.pool.query(`
|
|
367
|
+
SELECT
|
|
368
|
+
ugr.game_id,
|
|
369
|
+
ugr.role,
|
|
370
|
+
ugr.status,
|
|
371
|
+
ugr.claimed_at,
|
|
372
|
+
ugr.amount_claimed,
|
|
373
|
+
ugr.joined_at,
|
|
374
|
+
g.buy_in,
|
|
375
|
+
g.is_resolved,
|
|
376
|
+
g.created_by,
|
|
377
|
+
g.title,
|
|
378
|
+
g.sports_event->'winner'->>'walletAddress' as winner_wallet
|
|
379
|
+
FROM user_game_refs ugr
|
|
380
|
+
JOIN games g ON ugr.game_id = g.game_id
|
|
381
|
+
WHERE ugr.wallet_address = $1
|
|
382
|
+
AND g.game_type = 'billiards'
|
|
383
|
+
ORDER BY ugr.joined_at ASC
|
|
384
|
+
`, [walletAddress]);
|
|
385
|
+
|
|
386
|
+
if (result.rows.length === 0) return null;
|
|
387
|
+
|
|
388
|
+
let roomsCreated = 0;
|
|
389
|
+
let roomsJoined = 0;
|
|
390
|
+
let roomsWon = 0;
|
|
391
|
+
let roomsLost = 0;
|
|
392
|
+
let roomsPending = 0;
|
|
393
|
+
let totalWagered = 0; // Only resolved games
|
|
394
|
+
let pendingWagered = 0; // Pending games (not yet counted in P&L)
|
|
395
|
+
let totalWon = 0;
|
|
396
|
+
let biggestWin = 0;
|
|
397
|
+
let biggestWinRoomId = null;
|
|
398
|
+
let currentStreak = 0;
|
|
399
|
+
let longestStreak = 0;
|
|
400
|
+
let lastResult = null; // 'win' or 'loss'
|
|
401
|
+
|
|
402
|
+
for (const game of result.rows) {
|
|
403
|
+
const buyIn = parseFloat(game.buy_in) || 0;
|
|
404
|
+
|
|
405
|
+
// Track if user created this room
|
|
406
|
+
if (game.created_by === walletAddress) {
|
|
407
|
+
roomsCreated++;
|
|
408
|
+
} else {
|
|
409
|
+
roomsJoined++;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Check if game is resolved
|
|
413
|
+
if (!game.is_resolved) {
|
|
414
|
+
// Game still pending - DON'T count towards P&L yet
|
|
415
|
+
roomsPending++;
|
|
416
|
+
pendingWagered += buyIn;
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Only count wagered amount for RESOLVED games
|
|
421
|
+
totalWagered += buyIn;
|
|
422
|
+
|
|
423
|
+
// Determine if user won using status field or winner_wallet
|
|
424
|
+
const userWon = game.status === 'won' || game.winner_wallet === walletAddress;
|
|
425
|
+
|
|
426
|
+
if (userWon) {
|
|
427
|
+
roomsWon++;
|
|
428
|
+
|
|
429
|
+
// Calculate winnings from amount_claimed if available
|
|
430
|
+
const amountClaimed = parseFloat(game.amount_claimed) || 0;
|
|
431
|
+
const winAmount = amountClaimed > 0 ? amountClaimed : 0;
|
|
432
|
+
|
|
433
|
+
totalWon += winAmount;
|
|
434
|
+
if (winAmount > biggestWin) {
|
|
435
|
+
biggestWin = winAmount;
|
|
436
|
+
biggestWinRoomId = game.game_id;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Streak tracking
|
|
440
|
+
if (lastResult === 'win') {
|
|
441
|
+
currentStreak++;
|
|
442
|
+
} else {
|
|
443
|
+
currentStreak = 1;
|
|
444
|
+
}
|
|
445
|
+
lastResult = 'win';
|
|
446
|
+
if (currentStreak > longestStreak) {
|
|
447
|
+
longestStreak = currentStreak;
|
|
448
|
+
}
|
|
449
|
+
} else if (game.is_resolved) {
|
|
450
|
+
// Game resolved but user didn't win = loss
|
|
451
|
+
roomsLost++;
|
|
452
|
+
|
|
453
|
+
// Streak tracking
|
|
454
|
+
if (lastResult === 'loss') {
|
|
455
|
+
currentStreak--;
|
|
456
|
+
} else {
|
|
457
|
+
currentStreak = -1;
|
|
458
|
+
}
|
|
459
|
+
lastResult = 'loss';
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const roomsPlayed = roomsWon + roomsLost;
|
|
464
|
+
|
|
465
|
+
return {
|
|
466
|
+
roomsCreated,
|
|
467
|
+
roomsJoined,
|
|
468
|
+
roomsPlayed,
|
|
469
|
+
roomsWon,
|
|
470
|
+
roomsLost,
|
|
471
|
+
roomsPending,
|
|
472
|
+
totalWagered, // Only from resolved games
|
|
473
|
+
pendingWagered, // Amount at stake in pending games
|
|
474
|
+
totalWon,
|
|
475
|
+
netPNL: totalWon - totalWagered,
|
|
476
|
+
biggestWin,
|
|
477
|
+
biggestWinRoomId,
|
|
478
|
+
currentStreak,
|
|
479
|
+
longestStreak,
|
|
480
|
+
winRate: roomsPlayed > 0 ? (roomsWon / roomsPlayed * 100) : 0,
|
|
481
|
+
};
|
|
482
|
+
} catch (error) {
|
|
483
|
+
console.error('🎱 Error getting billiards stats:', error);
|
|
484
|
+
return null;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* 🔴 Get Connect4 game stats
|
|
490
|
+
*
|
|
491
|
+
* Connect4 uses game_type = 'connect4'
|
|
492
|
+
* Winners are determined by connect4_winner field ('red' or 'yellow')
|
|
493
|
+
* Red = creator (home), Yellow = joiner (away)
|
|
494
|
+
*/
|
|
495
|
+
async _getConnect4Stats(walletAddress) {
|
|
496
|
+
try {
|
|
497
|
+
// Get all connect4 games the user participated in
|
|
498
|
+
const result = await this.pool.query(`
|
|
499
|
+
SELECT
|
|
500
|
+
ugr.game_id,
|
|
501
|
+
ugr.role,
|
|
502
|
+
ugr.team_choice,
|
|
503
|
+
ugr.status,
|
|
504
|
+
ugr.claimed_at,
|
|
505
|
+
ugr.amount_claimed,
|
|
506
|
+
ugr.joined_at,
|
|
507
|
+
g.buy_in,
|
|
508
|
+
g.is_resolved,
|
|
509
|
+
g.game_status,
|
|
510
|
+
g.created_by,
|
|
511
|
+
g.title,
|
|
512
|
+
g.connect4_winner
|
|
513
|
+
FROM user_game_refs ugr
|
|
514
|
+
JOIN games g ON ugr.game_id = g.game_id
|
|
515
|
+
WHERE ugr.wallet_address = $1
|
|
516
|
+
AND g.game_type = 'connect4'
|
|
517
|
+
ORDER BY ugr.joined_at ASC
|
|
518
|
+
`, [walletAddress]);
|
|
519
|
+
|
|
520
|
+
if (result.rows.length === 0) return null;
|
|
521
|
+
|
|
522
|
+
let gamesCreated = 0;
|
|
523
|
+
let gamesJoined = 0;
|
|
524
|
+
let gamesWon = 0;
|
|
525
|
+
let gamesLost = 0;
|
|
526
|
+
let gamesPending = 0;
|
|
527
|
+
let totalWagered = 0; // Only resolved games
|
|
528
|
+
let pendingWagered = 0; // Pending games (not yet counted in P&L)
|
|
529
|
+
let totalWon = 0;
|
|
530
|
+
let biggestWin = 0;
|
|
531
|
+
let biggestWinGameId = null;
|
|
532
|
+
let currentStreak = 0;
|
|
533
|
+
let longestStreak = 0;
|
|
534
|
+
let lastResult = null; // 'win' or 'loss'
|
|
535
|
+
|
|
536
|
+
for (const game of result.rows) {
|
|
537
|
+
const buyIn = parseFloat(game.buy_in) || 0;
|
|
538
|
+
|
|
539
|
+
// Track if user created this game
|
|
540
|
+
if (game.created_by === walletAddress) {
|
|
541
|
+
gamesCreated++;
|
|
542
|
+
} else {
|
|
543
|
+
gamesJoined++;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Check if game is a valid completed game with a winner (not cancelled, not draw)
|
|
547
|
+
// Only count games where game_status = 'completed' and connect4_winner is 'home' or 'away'
|
|
548
|
+
const isValidCompletedGame = game.game_status === 'completed' &&
|
|
549
|
+
game.connect4_winner &&
|
|
550
|
+
(game.connect4_winner === 'home' || game.connect4_winner === 'away');
|
|
551
|
+
|
|
552
|
+
if (!isValidCompletedGame) {
|
|
553
|
+
// Game is pending, cancelled, or a draw - DON'T count towards W/L stats
|
|
554
|
+
gamesPending++;
|
|
555
|
+
pendingWagered += buyIn;
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Only count wagered amount for RESOLVED games
|
|
560
|
+
totalWagered += buyIn;
|
|
561
|
+
|
|
562
|
+
// Determine if user won
|
|
563
|
+
// Red = creator plays as 'red', Yellow = joiner plays as 'yellow'
|
|
564
|
+
// team_choice should be 'red' or 'yellow'
|
|
565
|
+
const userWon = game.team_choice === game.connect4_winner;
|
|
566
|
+
|
|
567
|
+
if (userWon) {
|
|
568
|
+
gamesWon++;
|
|
569
|
+
|
|
570
|
+
// Calculate winnings from amount_claimed if available
|
|
571
|
+
const amountClaimed = parseFloat(game.amount_claimed) || 0;
|
|
572
|
+
const winAmount = amountClaimed > 0 ? amountClaimed : 0;
|
|
573
|
+
|
|
574
|
+
totalWon += winAmount;
|
|
575
|
+
if (winAmount > biggestWin) {
|
|
576
|
+
biggestWin = winAmount;
|
|
577
|
+
biggestWinGameId = game.game_id;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Streak tracking
|
|
581
|
+
if (lastResult === 'win') {
|
|
582
|
+
currentStreak++;
|
|
583
|
+
} else {
|
|
584
|
+
currentStreak = 1;
|
|
585
|
+
}
|
|
586
|
+
lastResult = 'win';
|
|
587
|
+
if (currentStreak > longestStreak) {
|
|
588
|
+
longestStreak = currentStreak;
|
|
589
|
+
}
|
|
590
|
+
} else {
|
|
591
|
+
// Game resolved but user didn't win = loss
|
|
592
|
+
gamesLost++;
|
|
593
|
+
|
|
594
|
+
// Streak tracking
|
|
595
|
+
if (lastResult === 'loss') {
|
|
596
|
+
currentStreak--;
|
|
597
|
+
} else {
|
|
598
|
+
currentStreak = -1;
|
|
599
|
+
}
|
|
600
|
+
lastResult = 'loss';
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const gamesPlayed = gamesWon + gamesLost;
|
|
605
|
+
|
|
606
|
+
return {
|
|
607
|
+
gamesCreated,
|
|
608
|
+
gamesJoined,
|
|
609
|
+
gamesPlayed,
|
|
610
|
+
gamesWon,
|
|
611
|
+
gamesLost,
|
|
612
|
+
gamesPending,
|
|
613
|
+
totalWagered, // Only from resolved games
|
|
614
|
+
pendingWagered, // Amount at stake in pending games
|
|
615
|
+
totalWon,
|
|
616
|
+
netPNL: totalWon - totalWagered,
|
|
617
|
+
biggestWin,
|
|
618
|
+
biggestWinGameId,
|
|
619
|
+
currentStreak,
|
|
620
|
+
longestStreak,
|
|
621
|
+
winRate: gamesPlayed > 0 ? (gamesWon / gamesPlayed * 100) : 0,
|
|
622
|
+
};
|
|
623
|
+
} catch (error) {
|
|
624
|
+
console.error('🔴 Error getting Connect4 stats:', error);
|
|
625
|
+
return null;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Get sports betting stats (calculated from games tables)
|
|
631
|
+
*/
|
|
632
|
+
async _getSportsStats(walletAddress) {
|
|
633
|
+
// Get all sports games the user participated in
|
|
634
|
+
const result = await this.pool.query(`
|
|
635
|
+
SELECT
|
|
636
|
+
ugr.game_id,
|
|
637
|
+
ugr.role,
|
|
638
|
+
ugr.team_choice,
|
|
639
|
+
ugr.claimed_at,
|
|
640
|
+
ugr.amount_claimed,
|
|
641
|
+
ugr.joined_at,
|
|
642
|
+
g.buy_in,
|
|
643
|
+
g.is_resolved,
|
|
644
|
+
g.is_locked,
|
|
645
|
+
g.game_mode,
|
|
646
|
+
g.sports_event,
|
|
647
|
+
g.home_team_players,
|
|
648
|
+
g.away_team_players,
|
|
649
|
+
g.created_by
|
|
650
|
+
FROM user_game_refs ugr
|
|
651
|
+
JOIN games g ON ugr.game_id = g.game_id
|
|
652
|
+
WHERE ugr.wallet_address = $1
|
|
653
|
+
AND g.game_mode = 4
|
|
654
|
+
ORDER BY ugr.joined_at ASC
|
|
655
|
+
`, [walletAddress]);
|
|
656
|
+
|
|
657
|
+
if (result.rows.length === 0) return null;
|
|
658
|
+
|
|
659
|
+
let gamesCreated = 0;
|
|
660
|
+
let gamesJoined = 0;
|
|
661
|
+
let gamesWon = 0;
|
|
662
|
+
let gamesLost = 0;
|
|
663
|
+
let gamesPending = 0;
|
|
664
|
+
let totalWagered = 0; // Only resolved games
|
|
665
|
+
let pendingWagered = 0; // Pending games (not yet counted in P&L)
|
|
666
|
+
let totalWon = 0;
|
|
667
|
+
let biggestWin = 0;
|
|
668
|
+
let currentStreak = 0;
|
|
669
|
+
let longestStreak = 0;
|
|
670
|
+
let lastResult = null; // 'win' or 'loss'
|
|
671
|
+
|
|
672
|
+
for (const game of result.rows) {
|
|
673
|
+
const buyIn = parseFloat(game.buy_in) || 0;
|
|
674
|
+
|
|
675
|
+
// Track if user created this game
|
|
676
|
+
if (game.created_by === walletAddress) {
|
|
677
|
+
gamesCreated++;
|
|
678
|
+
} else {
|
|
679
|
+
gamesJoined++;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
if (!game.is_resolved) {
|
|
683
|
+
// Game still pending - DON'T count towards P&L yet
|
|
684
|
+
gamesPending++;
|
|
685
|
+
pendingWagered += buyIn;
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Check if both teams have players (if not, it's a refund scenario)
|
|
690
|
+
const homeCount = game.home_team_players?.length || 0;
|
|
691
|
+
const awayCount = game.away_team_players?.length || 0;
|
|
692
|
+
|
|
693
|
+
// If either side has no players, this is a refund - don't count in win/loss stats
|
|
694
|
+
if (homeCount === 0 || awayCount === 0) {
|
|
695
|
+
console.log(`📊 Skipping refund game ${game.game_id} - no competition (home: ${homeCount}, away: ${awayCount})`);
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// Only count wagered amount for RESOLVED games with actual competition
|
|
700
|
+
totalWagered += buyIn;
|
|
701
|
+
|
|
702
|
+
// Game is resolved - check if user won
|
|
703
|
+
const finalScore = game.sports_event?.finalScore;
|
|
704
|
+
if (finalScore && game.team_choice) {
|
|
705
|
+
const userWon = game.team_choice === finalScore.winner;
|
|
706
|
+
|
|
707
|
+
if (userWon) {
|
|
708
|
+
gamesWon++;
|
|
709
|
+
|
|
710
|
+
// Calculate winnings
|
|
711
|
+
// Total pot = buy_in * total_players
|
|
712
|
+
// Winner share = pot * (1 - fee) / winners_count
|
|
713
|
+
const totalPlayers = homeCount + awayCount;
|
|
714
|
+
const totalPot = buyIn * totalPlayers;
|
|
715
|
+
const feePercent = 0.12; // 10% operator + 2% oracle
|
|
716
|
+
const winnersCount = finalScore.winner === 'home' ? homeCount : awayCount;
|
|
717
|
+
const winShare = winnersCount > 0 ? (totalPot * (1 - feePercent)) / winnersCount : 0;
|
|
718
|
+
|
|
719
|
+
totalWon += winShare;
|
|
720
|
+
if (winShare > biggestWin) {
|
|
721
|
+
biggestWin = winShare;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// Streak tracking
|
|
725
|
+
if (lastResult === 'win') {
|
|
726
|
+
currentStreak++;
|
|
727
|
+
} else {
|
|
728
|
+
currentStreak = 1;
|
|
729
|
+
}
|
|
730
|
+
lastResult = 'win';
|
|
731
|
+
if (currentStreak > longestStreak) {
|
|
732
|
+
longestStreak = currentStreak;
|
|
733
|
+
}
|
|
734
|
+
} else {
|
|
735
|
+
gamesLost++;
|
|
736
|
+
// Lost - streak broken
|
|
737
|
+
if (lastResult === 'loss') {
|
|
738
|
+
currentStreak--;
|
|
739
|
+
} else {
|
|
740
|
+
currentStreak = -1;
|
|
741
|
+
}
|
|
742
|
+
lastResult = 'loss';
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
return {
|
|
748
|
+
gamesCreated,
|
|
749
|
+
gamesJoined,
|
|
750
|
+
gamesPlayed: gamesWon + gamesLost,
|
|
751
|
+
gamesWon,
|
|
752
|
+
gamesLost,
|
|
753
|
+
gamesPending,
|
|
754
|
+
totalWagered, // Only from resolved games
|
|
755
|
+
pendingWagered, // Amount at stake in pending games
|
|
756
|
+
totalWon,
|
|
757
|
+
netPNL: totalWon - totalWagered,
|
|
758
|
+
biggestWin,
|
|
759
|
+
currentStreak,
|
|
760
|
+
longestStreak,
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Get recent games for display
|
|
766
|
+
* Handles sports games, billiards/pool rooms, Connect4, AND Jackpot rounds
|
|
767
|
+
* Supports legacy, hybrid, and pari-mutuel betting modes
|
|
768
|
+
*/
|
|
769
|
+
async _getRecentGames(walletAddress, limit = 10) {
|
|
770
|
+
// Fetch regular games and jackpot entries in parallel
|
|
771
|
+
const [gamesResult, jackpotResult] = await Promise.all([
|
|
772
|
+
// Note: For billiards, winner is stored in sports_event->'winner'->>'walletAddress'
|
|
773
|
+
this.pool.query(`
|
|
774
|
+
SELECT
|
|
775
|
+
ugr.game_id,
|
|
776
|
+
ugr.team_choice,
|
|
777
|
+
ugr.status,
|
|
778
|
+
ugr.joined_at,
|
|
779
|
+
ugr.claimed_at,
|
|
780
|
+
ugr.amount_claimed,
|
|
781
|
+
g.title,
|
|
782
|
+
g.buy_in,
|
|
783
|
+
g.is_resolved,
|
|
784
|
+
g.sports_event,
|
|
785
|
+
g.home_team_players,
|
|
786
|
+
g.away_team_players,
|
|
787
|
+
g.game_type,
|
|
788
|
+
g.sports_event->'winner'->>'walletAddress' as billiards_winner_wallet,
|
|
789
|
+
g.connect4_winner,
|
|
790
|
+
g.total_pool,
|
|
791
|
+
g.home_pool,
|
|
792
|
+
g.away_pool,
|
|
793
|
+
g.draw_pool,
|
|
794
|
+
g.player_amounts
|
|
795
|
+
FROM user_game_refs ugr
|
|
796
|
+
JOIN games g ON ugr.game_id = g.game_id
|
|
797
|
+
WHERE ugr.wallet_address = $1
|
|
798
|
+
ORDER BY ugr.joined_at DESC
|
|
799
|
+
LIMIT $2
|
|
800
|
+
`, [walletAddress, limit]),
|
|
801
|
+
// Jackpot entries joined with round results
|
|
802
|
+
this.pool.query(`
|
|
803
|
+
SELECT
|
|
804
|
+
je.round_id,
|
|
805
|
+
je.amount as entry_amount,
|
|
806
|
+
je.created_at as entered_at,
|
|
807
|
+
jr.winner,
|
|
808
|
+
jr.win_amount,
|
|
809
|
+
jr.total_pot,
|
|
810
|
+
jr.entry_count,
|
|
811
|
+
jr.timestamp as resolved_at
|
|
812
|
+
FROM jackpot_entries je
|
|
813
|
+
LEFT JOIN jackpot_rounds jr ON je.round_id = jr.round_id
|
|
814
|
+
WHERE je.wallet_address = $1
|
|
815
|
+
ORDER BY je.created_at DESC
|
|
816
|
+
LIMIT $2
|
|
817
|
+
`, [walletAddress, limit]),
|
|
818
|
+
]);
|
|
819
|
+
|
|
820
|
+
const result = gamesResult;
|
|
821
|
+
|
|
822
|
+
// Build jackpot recent games
|
|
823
|
+
const jackpotGames = jackpotResult.rows.map(entry => {
|
|
824
|
+
const buyInLamports = parseInt(entry.entry_amount) || 0;
|
|
825
|
+
const buyInSOL = buyInLamports / 1e9;
|
|
826
|
+
const isResolved = !!entry.resolved_at;
|
|
827
|
+
const userWon = isResolved && entry.winner === walletAddress;
|
|
828
|
+
|
|
829
|
+
let resultStatus = 'pending';
|
|
830
|
+
let pnl = 0;
|
|
831
|
+
|
|
832
|
+
if (isResolved) {
|
|
833
|
+
if (userWon) {
|
|
834
|
+
resultStatus = 'won';
|
|
835
|
+
const winAmountSOL = (parseInt(entry.win_amount) || 0) / 1e9;
|
|
836
|
+
pnl = winAmountSOL - buyInSOL;
|
|
837
|
+
} else {
|
|
838
|
+
resultStatus = 'lost';
|
|
839
|
+
pnl = -buyInSOL;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
return {
|
|
844
|
+
gameId: `jackpot_${entry.round_id}`,
|
|
845
|
+
title: `Jackpot Round #${entry.round_id}`,
|
|
846
|
+
gameType: 'jackpot',
|
|
847
|
+
teamChoice: null,
|
|
848
|
+
buyIn: parseFloat(buyInSOL.toFixed(4)),
|
|
849
|
+
result: resultStatus,
|
|
850
|
+
pnl: parseFloat(pnl.toFixed(4)),
|
|
851
|
+
finalScore: null,
|
|
852
|
+
playedAt: entry.entered_at,
|
|
853
|
+
claimed: isResolved,
|
|
854
|
+
};
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
// Build regular recent games
|
|
858
|
+
const regularGames = result.rows.map(game => {
|
|
859
|
+
const defaultBuyIn = parseFloat(game.buy_in) || 0;
|
|
860
|
+
const finalScore = game.sports_event?.finalScore;
|
|
861
|
+
const isBilliards = game.game_type === 'billiards';
|
|
862
|
+
const isConnect4 = game.game_type === 'connect4';
|
|
863
|
+
|
|
864
|
+
// Determine user's actual bet amount (pari-mutuel vs legacy)
|
|
865
|
+
const playerAmounts = game.player_amounts || {};
|
|
866
|
+
const userBetAmount = playerAmounts[walletAddress] !== undefined
|
|
867
|
+
? parseFloat(playerAmounts[walletAddress])
|
|
868
|
+
: defaultBuyIn;
|
|
869
|
+
|
|
870
|
+
let resultStatus = 'pending';
|
|
871
|
+
let pnl = 0;
|
|
872
|
+
|
|
873
|
+
// 🎱 Handle billiards games
|
|
874
|
+
if (isBilliards) {
|
|
875
|
+
if (game.is_resolved) {
|
|
876
|
+
// Check if user won via status field or billiards_winner_wallet (from sports_event JSON)
|
|
877
|
+
const userWon = game.status === 'won' || game.billiards_winner_wallet === walletAddress;
|
|
878
|
+
|
|
879
|
+
if (userWon) {
|
|
880
|
+
resultStatus = 'won';
|
|
881
|
+
const amountClaimed = parseFloat(game.amount_claimed) || 0;
|
|
882
|
+
pnl = amountClaimed > 0 ? amountClaimed - userBetAmount : 0;
|
|
883
|
+
} else {
|
|
884
|
+
resultStatus = 'lost';
|
|
885
|
+
pnl = -userBetAmount;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
// else remains 'pending'
|
|
889
|
+
}
|
|
890
|
+
// 🎮 Handle Connect4 games
|
|
891
|
+
else if (isConnect4) {
|
|
892
|
+
if (game.is_resolved && game.connect4_winner) {
|
|
893
|
+
const userWon = game.team_choice === game.connect4_winner;
|
|
894
|
+
|
|
895
|
+
if (userWon) {
|
|
896
|
+
resultStatus = 'won';
|
|
897
|
+
const amountClaimed = parseFloat(game.amount_claimed) || 0;
|
|
898
|
+
pnl = amountClaimed > 0 ? amountClaimed - userBetAmount : 0;
|
|
899
|
+
} else {
|
|
900
|
+
resultStatus = 'lost';
|
|
901
|
+
pnl = -userBetAmount;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
// else remains 'pending'
|
|
905
|
+
}
|
|
906
|
+
// 🏀 Handle sports games (supports legacy, hybrid, pari-mutuel)
|
|
907
|
+
else if (game.is_resolved && finalScore && game.team_choice) {
|
|
908
|
+
const homeCount = game.home_team_players?.length || 0;
|
|
909
|
+
const awayCount = game.away_team_players?.length || 0;
|
|
910
|
+
|
|
911
|
+
// Check for refund scenario (no competition)
|
|
912
|
+
if (homeCount === 0 || awayCount === 0) {
|
|
913
|
+
resultStatus = 'refunded';
|
|
914
|
+
pnl = 0; // No loss, refunded
|
|
915
|
+
} else {
|
|
916
|
+
const userWon = game.team_choice === finalScore.winner;
|
|
917
|
+
|
|
918
|
+
if (userWon) {
|
|
919
|
+
resultStatus = 'won';
|
|
920
|
+
|
|
921
|
+
// Check if this is a pari-mutuel game (has player_amounts with entries)
|
|
922
|
+
const isPariMutuel = Object.keys(playerAmounts).length > 0;
|
|
923
|
+
|
|
924
|
+
if (isPariMutuel) {
|
|
925
|
+
// Pari-mutuel: proportional payout based on user's share of winning pool
|
|
926
|
+
const totalPool = parseFloat(game.total_pool) || 0;
|
|
927
|
+
const homePool = parseFloat(game.home_pool) || 0;
|
|
928
|
+
const awayPool = parseFloat(game.away_pool) || 0;
|
|
929
|
+
const drawPool = parseFloat(game.draw_pool) || 0;
|
|
930
|
+
|
|
931
|
+
const winningPool = finalScore.winner === 'home' ? homePool
|
|
932
|
+
: finalScore.winner === 'away' ? awayPool
|
|
933
|
+
: drawPool;
|
|
934
|
+
|
|
935
|
+
const feePercent = 0.06; // 6% fee (5% platform + 1% oracle)
|
|
936
|
+
const netPool = totalPool * (1 - feePercent);
|
|
937
|
+
|
|
938
|
+
// User's share = (userBet / winningPool) * netPool
|
|
939
|
+
const userShare = winningPool > 0 ? (userBetAmount / winningPool) * netPool : 0;
|
|
940
|
+
pnl = userShare - userBetAmount;
|
|
941
|
+
} else {
|
|
942
|
+
// Legacy: equal split among winners
|
|
943
|
+
const totalPlayers = homeCount + awayCount;
|
|
944
|
+
const totalPot = defaultBuyIn * totalPlayers;
|
|
945
|
+
const feePercent = 0.06; // 6% fee
|
|
946
|
+
const winnersCount = finalScore.winner === 'home' ? homeCount : awayCount;
|
|
947
|
+
const winShare = winnersCount > 0 ? (totalPot * (1 - feePercent)) / winnersCount : 0;
|
|
948
|
+
pnl = winShare - defaultBuyIn;
|
|
949
|
+
}
|
|
950
|
+
} else {
|
|
951
|
+
resultStatus = 'lost';
|
|
952
|
+
pnl = -userBetAmount;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// For pending games, show event date. For resolved, show when user joined.
|
|
958
|
+
const eventDate = game.sports_event?.strTimestamp;
|
|
959
|
+
const displayDate = resultStatus === 'pending' && eventDate
|
|
960
|
+
? new Date(eventDate + 'Z') // Event date for pending
|
|
961
|
+
: game.joined_at; // Joined date for resolved
|
|
962
|
+
|
|
963
|
+
return {
|
|
964
|
+
gameId: game.game_id,
|
|
965
|
+
title: game.title,
|
|
966
|
+
gameType: game.game_type || 'sports', // Include game type for display
|
|
967
|
+
teamChoice: game.team_choice,
|
|
968
|
+
buyIn: parseFloat(userBetAmount.toFixed(4)), // Show user's actual bet, not default
|
|
969
|
+
result: resultStatus,
|
|
970
|
+
pnl: parseFloat(pnl.toFixed(4)),
|
|
971
|
+
finalScore: finalScore ? {
|
|
972
|
+
winner: finalScore.winner,
|
|
973
|
+
homeScore: finalScore.homeScore,
|
|
974
|
+
awayScore: finalScore.awayScore,
|
|
975
|
+
} : null,
|
|
976
|
+
playedAt: displayDate,
|
|
977
|
+
claimed: !!game.claimed_at,
|
|
978
|
+
};
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
// Merge regular games + jackpot games, sort by date desc, limit
|
|
982
|
+
const allGames = [...regularGames, ...jackpotGames]
|
|
983
|
+
.sort((a, b) => new Date(b.playedAt).getTime() - new Date(a.playedAt).getTime())
|
|
984
|
+
.slice(0, limit);
|
|
985
|
+
|
|
986
|
+
return allGames;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
/**
|
|
990
|
+
* Record a sports betting win (called when game is resolved)
|
|
991
|
+
*/
|
|
992
|
+
async recordSportsBetResult({ walletAddress, gameId, buyIn, won, winAmount }) {
|
|
993
|
+
try {
|
|
994
|
+
const pnlChange = won ? (winAmount - buyIn) : -buyIn;
|
|
995
|
+
|
|
996
|
+
await this.pool.query(`
|
|
997
|
+
INSERT INTO sports_betting_stats (
|
|
998
|
+
wallet_address,
|
|
999
|
+
games_joined,
|
|
1000
|
+
games_won,
|
|
1001
|
+
games_lost,
|
|
1002
|
+
total_wagered_sol,
|
|
1003
|
+
total_won_sol,
|
|
1004
|
+
net_pnl_sol,
|
|
1005
|
+
biggest_win_sol,
|
|
1006
|
+
biggest_win_game_id,
|
|
1007
|
+
current_win_streak,
|
|
1008
|
+
longest_win_streak,
|
|
1009
|
+
last_played_at
|
|
1010
|
+
) VALUES ($1, 1, $2, $3, $4, $5, $6, $7, $8, $9, $9, NOW())
|
|
1011
|
+
ON CONFLICT (wallet_address) DO UPDATE SET
|
|
1012
|
+
games_joined = sports_betting_stats.games_joined + 1,
|
|
1013
|
+
games_won = sports_betting_stats.games_won + $2,
|
|
1014
|
+
games_lost = sports_betting_stats.games_lost + $3,
|
|
1015
|
+
total_wagered_sol = sports_betting_stats.total_wagered_sol + $4,
|
|
1016
|
+
total_won_sol = sports_betting_stats.total_won_sol + $5,
|
|
1017
|
+
net_pnl_sol = sports_betting_stats.net_pnl_sol + $6,
|
|
1018
|
+
biggest_win_sol = GREATEST(sports_betting_stats.biggest_win_sol, $7),
|
|
1019
|
+
biggest_win_game_id = CASE
|
|
1020
|
+
WHEN $7 > sports_betting_stats.biggest_win_sol THEN $8
|
|
1021
|
+
ELSE sports_betting_stats.biggest_win_game_id
|
|
1022
|
+
END,
|
|
1023
|
+
current_win_streak = CASE
|
|
1024
|
+
WHEN $2 = 1 THEN sports_betting_stats.current_win_streak + 1
|
|
1025
|
+
ELSE 0
|
|
1026
|
+
END,
|
|
1027
|
+
longest_win_streak = GREATEST(
|
|
1028
|
+
sports_betting_stats.longest_win_streak,
|
|
1029
|
+
CASE WHEN $2 = 1 THEN sports_betting_stats.current_win_streak + 1 ELSE 0 END
|
|
1030
|
+
),
|
|
1031
|
+
last_played_at = NOW(),
|
|
1032
|
+
updated_at = NOW()
|
|
1033
|
+
`, [
|
|
1034
|
+
walletAddress,
|
|
1035
|
+
won ? 1 : 0,
|
|
1036
|
+
won ? 0 : 1,
|
|
1037
|
+
buyIn,
|
|
1038
|
+
won ? winAmount : 0,
|
|
1039
|
+
pnlChange,
|
|
1040
|
+
won ? winAmount : 0,
|
|
1041
|
+
won ? gameId : null,
|
|
1042
|
+
won ? 1 : 0,
|
|
1043
|
+
]);
|
|
1044
|
+
|
|
1045
|
+
// Invalidate cache
|
|
1046
|
+
this.cache.delete(walletAddress);
|
|
1047
|
+
|
|
1048
|
+
console.log(`📊 Recorded sports bet result: ${walletAddress.slice(0, 8)}... ${won ? 'WON' : 'LOST'} ◎${buyIn}`);
|
|
1049
|
+
} catch (error) {
|
|
1050
|
+
console.error('Error recording sports bet result:', error);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
/**
|
|
1055
|
+
* Get leaderboard with combined stats
|
|
1056
|
+
*/
|
|
1057
|
+
async getLeaderboard(limit = 20, sortBy = 'pnl') {
|
|
1058
|
+
try {
|
|
1059
|
+
// Get all users with any stats
|
|
1060
|
+
const result = await this.pool.query(`
|
|
1061
|
+
SELECT DISTINCT wallet_address FROM (
|
|
1062
|
+
SELECT wallet_address FROM player_stats
|
|
1063
|
+
UNION
|
|
1064
|
+
SELECT wallet_address FROM user_game_refs
|
|
1065
|
+
) combined
|
|
1066
|
+
LIMIT 100
|
|
1067
|
+
`);
|
|
1068
|
+
|
|
1069
|
+
const wallets = result.rows.map(r => r.wallet_address);
|
|
1070
|
+
|
|
1071
|
+
// Fetch stats for all users in parallel
|
|
1072
|
+
const statsPromises = wallets.map(wallet => this.getUserProfileStats(wallet));
|
|
1073
|
+
const allStats = await Promise.all(statsPromises);
|
|
1074
|
+
|
|
1075
|
+
// Sort based on criteria
|
|
1076
|
+
const sorted = allStats.sort((a, b) => {
|
|
1077
|
+
if (sortBy === 'pnl') {
|
|
1078
|
+
return b.summary.netPNL - a.summary.netPNL;
|
|
1079
|
+
} else if (sortBy === 'wagered') {
|
|
1080
|
+
return b.summary.totalWagered - a.summary.totalWagered;
|
|
1081
|
+
} else if (sortBy === 'wins') {
|
|
1082
|
+
return b.summary.totalGamesWon - a.summary.totalGamesWon;
|
|
1083
|
+
} else if (sortBy === 'winrate') {
|
|
1084
|
+
return b.summary.winRate - a.summary.winRate;
|
|
1085
|
+
}
|
|
1086
|
+
return 0;
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
return sorted.slice(0, limit).map((stats, index) => ({
|
|
1090
|
+
rank: index + 1,
|
|
1091
|
+
walletAddress: stats.walletAddress,
|
|
1092
|
+
username: stats.username,
|
|
1093
|
+
avatar: stats.avatar,
|
|
1094
|
+
netPNL: stats.summary.netPNL,
|
|
1095
|
+
totalWagered: stats.summary.totalWagered,
|
|
1096
|
+
totalGamesPlayed: stats.summary.totalGamesPlayed,
|
|
1097
|
+
totalGamesWon: stats.summary.totalGamesWon,
|
|
1098
|
+
winRate: stats.summary.winRate,
|
|
1099
|
+
}));
|
|
1100
|
+
} catch (error) {
|
|
1101
|
+
console.error('Error fetching leaderboard:', error);
|
|
1102
|
+
return [];
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
/**
|
|
1107
|
+
* Clear cache for a specific wallet (call after game results)
|
|
1108
|
+
*/
|
|
1109
|
+
invalidateCache(walletAddress) {
|
|
1110
|
+
this.cache.delete(walletAddress);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
/**
|
|
1114
|
+
* Clear all cache
|
|
1115
|
+
*/
|
|
1116
|
+
clearCache() {
|
|
1117
|
+
this.cache.clear();
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
/**
|
|
1121
|
+
* Get a user's friends list by wallet address (public)
|
|
1122
|
+
* Returns friends with their basic stats
|
|
1123
|
+
*/
|
|
1124
|
+
async getUserFriends(walletAddress, limit = 20) {
|
|
1125
|
+
try {
|
|
1126
|
+
const result = await this.pool.query(`
|
|
1127
|
+
SELECT
|
|
1128
|
+
u2.wallet_address,
|
|
1129
|
+
u2.username,
|
|
1130
|
+
u2.avatar,
|
|
1131
|
+
ur.created_at as friends_since
|
|
1132
|
+
FROM users u1
|
|
1133
|
+
JOIN user_relationships ur ON u1.id = ur.user_id
|
|
1134
|
+
JOIN users u2 ON ur.target_user_id = u2.id
|
|
1135
|
+
WHERE u1.wallet_address = $1
|
|
1136
|
+
AND ur.relationship_type = 'friend'
|
|
1137
|
+
ORDER BY ur.created_at DESC
|
|
1138
|
+
LIMIT $2
|
|
1139
|
+
`, [walletAddress, limit]);
|
|
1140
|
+
|
|
1141
|
+
return result.rows.map(row => ({
|
|
1142
|
+
walletAddress: row.wallet_address,
|
|
1143
|
+
username: row.username,
|
|
1144
|
+
avatar: row.avatar,
|
|
1145
|
+
friendsSince: row.friends_since,
|
|
1146
|
+
}));
|
|
1147
|
+
} catch (error) {
|
|
1148
|
+
console.error('Error getting user friends:', error);
|
|
1149
|
+
return [];
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
/**
|
|
1154
|
+
* Get mutual friends between two users
|
|
1155
|
+
*/
|
|
1156
|
+
async getMutualFriends(walletAddress1, walletAddress2) {
|
|
1157
|
+
try {
|
|
1158
|
+
const result = await this.pool.query(`
|
|
1159
|
+
SELECT DISTINCT
|
|
1160
|
+
u3.wallet_address,
|
|
1161
|
+
u3.username,
|
|
1162
|
+
u3.avatar
|
|
1163
|
+
FROM users u1
|
|
1164
|
+
JOIN user_relationships ur1 ON u1.id = ur1.user_id AND ur1.relationship_type = 'friend'
|
|
1165
|
+
JOIN users u2 ON u2.wallet_address = $2
|
|
1166
|
+
JOIN user_relationships ur2 ON u2.id = ur2.user_id AND ur2.relationship_type = 'friend'
|
|
1167
|
+
JOIN users u3 ON ur1.target_user_id = u3.id AND ur2.target_user_id = u3.id
|
|
1168
|
+
WHERE u1.wallet_address = $1
|
|
1169
|
+
LIMIT 10
|
|
1170
|
+
`, [walletAddress1, walletAddress2]);
|
|
1171
|
+
|
|
1172
|
+
return result.rows.map(row => ({
|
|
1173
|
+
walletAddress: row.wallet_address,
|
|
1174
|
+
username: row.username,
|
|
1175
|
+
avatar: row.avatar,
|
|
1176
|
+
}));
|
|
1177
|
+
} catch (error) {
|
|
1178
|
+
console.error('Error getting mutual friends:', error);
|
|
1179
|
+
return [];
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
module.exports = UserProfileStatsService;
|
|
1185
|
+
|