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,728 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 💰 Referral Earnings Service
|
|
3
|
+
*
|
|
4
|
+
* Handles referral commission calculation, storage, and payouts.
|
|
5
|
+
*
|
|
6
|
+
* ⚠️ IMPORTANT: Commission goes to the GAME CREATOR's referrer ONLY!
|
|
7
|
+
* NOT to every player's referrer.
|
|
8
|
+
*
|
|
9
|
+
* Fee Structure:
|
|
10
|
+
* - The person who referred the GAME CREATOR earns 1% of the ENTIRE POT
|
|
11
|
+
* - Commission comes from the operator's 5% fee (now 4% + 1% referral)
|
|
12
|
+
* - If game creator has no referrer, operator keeps the full 5%
|
|
13
|
+
*
|
|
14
|
+
* Example (10-player game, 0.5 SOL buy-in each = 5 SOL pot):
|
|
15
|
+
* - Game creator was referred by UserX
|
|
16
|
+
* - UserX earns 0.05 SOL (1% of 5 SOL pot)
|
|
17
|
+
* - Operator gets 0.20 SOL (4% of 5 SOL) instead of 0.25 SOL (5%)
|
|
18
|
+
* - Other players' referrers get NOTHING from this game
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const { pool } = require('./db'); // Shared database pool
|
|
22
|
+
|
|
23
|
+
// Configuration
|
|
24
|
+
const REFERRAL_COMMISSION_RATE = 0.01; // 1%
|
|
25
|
+
const LAMPORTS_PER_SOL = 1_000_000_000;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get referrer info for a wallet address
|
|
29
|
+
* Returns null if the user has no referrer
|
|
30
|
+
*/
|
|
31
|
+
async function getReferrerForWallet(walletAddress) {
|
|
32
|
+
try {
|
|
33
|
+
const result = await pool.query(`
|
|
34
|
+
SELECT
|
|
35
|
+
u.referral_code,
|
|
36
|
+
referrer.id as referrer_user_id,
|
|
37
|
+
referrer.wallet_address as referrer_wallet,
|
|
38
|
+
referrer.username as referrer_username
|
|
39
|
+
FROM users u
|
|
40
|
+
LEFT JOIN users referrer ON u.referral_code = referrer.my_referral_code
|
|
41
|
+
WHERE u.wallet_address = $1
|
|
42
|
+
AND u.referral_code IS NOT NULL
|
|
43
|
+
AND referrer.my_referral_code IS NOT NULL
|
|
44
|
+
`, [walletAddress]);
|
|
45
|
+
|
|
46
|
+
if (result.rows.length === 0) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return result.rows[0];
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error('[ReferralEarnings] Error getting referrer:', error.message);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Calculate and store referral commission for the GAME CREATOR's referrer
|
|
59
|
+
* Called after a game is resolved by the oracle
|
|
60
|
+
*
|
|
61
|
+
* ⚠️ IMPORTANT: Only the GAME CREATOR's referrer earns commission!
|
|
62
|
+
* They earn 1% of the ENTIRE POT, not just the creator's buy-in.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} gameId - The game ID
|
|
65
|
+
* @param {string} gameType - 'sports', 'billiards', 'jackpot'
|
|
66
|
+
* @param {number} potSizeLamports - Total pot size in lamports
|
|
67
|
+
* @returns {Object} - Summary of commission processed
|
|
68
|
+
*/
|
|
69
|
+
async function processGameCommissions(gameId, gameType, potSizeLamports) {
|
|
70
|
+
const startTime = Date.now();
|
|
71
|
+
const logPrefix = `[PROCESS-COMMISSION:${gameId.slice(-8)}]`;
|
|
72
|
+
|
|
73
|
+
console.log(`${logPrefix} ════════════════════════════════════════════════════`);
|
|
74
|
+
console.log(`${logPrefix} 📊 Processing referral commission for game`);
|
|
75
|
+
console.log(`${logPrefix} Full Game ID: ${gameId}`);
|
|
76
|
+
console.log(`${logPrefix} Game Type: ${gameType}`);
|
|
77
|
+
console.log(`${logPrefix} Pot Size: ${potSizeLamports} lamports (${potSizeLamports / LAMPORTS_PER_SOL} SOL)`);
|
|
78
|
+
console.log(`${logPrefix} ════════════════════════════════════════════════════`);
|
|
79
|
+
|
|
80
|
+
const summary = {
|
|
81
|
+
gameId,
|
|
82
|
+
gameType,
|
|
83
|
+
potSizeLamports,
|
|
84
|
+
gameCreator: null,
|
|
85
|
+
creatorHasReferrer: false,
|
|
86
|
+
totalCommissionsLamports: 0,
|
|
87
|
+
commissions: []
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
// Step 1: Get the GAME CREATOR from the games table
|
|
92
|
+
console.log(`${logPrefix} Step 1: Looking up game creator and their referrer...`);
|
|
93
|
+
const gameResult = await pool.query(`
|
|
94
|
+
SELECT
|
|
95
|
+
g.created_by as creator_wallet,
|
|
96
|
+
g.title,
|
|
97
|
+
u.id as creator_user_id,
|
|
98
|
+
u.username as creator_username,
|
|
99
|
+
u.referral_code,
|
|
100
|
+
referrer.id as referrer_user_id,
|
|
101
|
+
referrer.wallet_address as referrer_wallet,
|
|
102
|
+
referrer.username as referrer_username,
|
|
103
|
+
referrer.my_referral_code as referrer_my_code
|
|
104
|
+
FROM games g
|
|
105
|
+
JOIN users u ON g.created_by = u.wallet_address
|
|
106
|
+
LEFT JOIN users referrer ON u.referral_code = referrer.my_referral_code
|
|
107
|
+
WHERE g.game_id = $1
|
|
108
|
+
`, [gameId]);
|
|
109
|
+
|
|
110
|
+
console.log(`${logPrefix} Query returned ${gameResult.rows.length} row(s)`);
|
|
111
|
+
|
|
112
|
+
if (gameResult.rows.length === 0) {
|
|
113
|
+
console.log(`${logPrefix} ⚠️ Game ${gameId} not found in database - cannot process commission`);
|
|
114
|
+
console.log(`${logPrefix} This could mean: game deleted, or game_id mismatch`);
|
|
115
|
+
return summary;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const game = gameResult.rows[0];
|
|
119
|
+
summary.gameCreator = game.creator_wallet;
|
|
120
|
+
|
|
121
|
+
console.log(`${logPrefix} Step 2: Analyzing game creator's referral status`);
|
|
122
|
+
console.log(`${logPrefix} Game title: ${game.title || 'untitled'}`);
|
|
123
|
+
console.log(`${logPrefix} Creator: ${game.creator_username || 'unknown'} (${game.creator_wallet})`);
|
|
124
|
+
console.log(`${logPrefix} Creator's referral_code: ${game.referral_code || 'NULL (never referred)'}`);
|
|
125
|
+
|
|
126
|
+
// Step 2: Check if the game creator has a referrer
|
|
127
|
+
if (!game.referrer_wallet) {
|
|
128
|
+
console.log(`${logPrefix} ℹ️ Game creator has NO referrer`);
|
|
129
|
+
if (game.referral_code) {
|
|
130
|
+
console.log(`${logPrefix} ⚠️ Creator has referral_code="${game.referral_code}" but no matching referrer!`);
|
|
131
|
+
console.log(`${logPrefix} Possible causes: referrer account deleted, referrer's my_referral_code changed`);
|
|
132
|
+
} else {
|
|
133
|
+
console.log(`${logPrefix} Creator signed up without a referral code - operator keeps full 5%`);
|
|
134
|
+
}
|
|
135
|
+
return summary;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
summary.creatorHasReferrer = true;
|
|
139
|
+
console.log(`${logPrefix} ✅ FOUND REFERRER!`);
|
|
140
|
+
console.log(`${logPrefix} Referrer: ${game.referrer_username || 'unknown'} (${game.referrer_wallet})`);
|
|
141
|
+
console.log(`${logPrefix} Match: creator.referral_code(${game.referral_code}) = referrer.my_referral_code(${game.referrer_my_code})`);
|
|
142
|
+
|
|
143
|
+
// Step 3: Calculate commission (1% of ENTIRE POT)
|
|
144
|
+
console.log(`${logPrefix} Step 3: Calculating commission (1% of pot)`);
|
|
145
|
+
const commissionLamports = Math.floor(potSizeLamports * REFERRAL_COMMISSION_RATE);
|
|
146
|
+
console.log(`${logPrefix} ${potSizeLamports} × ${REFERRAL_COMMISSION_RATE} = ${commissionLamports} lamports`);
|
|
147
|
+
console.log(`${logPrefix} Commission: ${commissionLamports / LAMPORTS_PER_SOL} SOL`);
|
|
148
|
+
|
|
149
|
+
if (commissionLamports <= 0) {
|
|
150
|
+
console.log(`${logPrefix} ⚠️ Skipping - commission is 0 or negative (pot too small)`);
|
|
151
|
+
return summary;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Step 4: Insert earning record (one per game, for the creator's referrer)
|
|
155
|
+
console.log(`${logPrefix} Step 4: Inserting referral_earnings record...`);
|
|
156
|
+
console.log(`${logPrefix} NOTE: This creates a 'pending' status record WITHOUT tx signature`);
|
|
157
|
+
console.log(`${logPrefix} The oracle's recordReferralCommission should update this with signature later`);
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const insertResult = await pool.query(`
|
|
161
|
+
INSERT INTO referral_earnings (
|
|
162
|
+
referrer_user_id,
|
|
163
|
+
referrer_wallet,
|
|
164
|
+
referee_user_id,
|
|
165
|
+
referee_wallet,
|
|
166
|
+
game_id,
|
|
167
|
+
game_type,
|
|
168
|
+
pot_size,
|
|
169
|
+
referee_buy_in,
|
|
170
|
+
referee_won,
|
|
171
|
+
referee_payout,
|
|
172
|
+
commission_rate,
|
|
173
|
+
commission_amount,
|
|
174
|
+
status
|
|
175
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, 'pending')
|
|
176
|
+
ON CONFLICT (referee_wallet, game_id) DO NOTHING
|
|
177
|
+
RETURNING id
|
|
178
|
+
`, [
|
|
179
|
+
game.referrer_user_id,
|
|
180
|
+
game.referrer_wallet,
|
|
181
|
+
game.creator_user_id,
|
|
182
|
+
game.creator_wallet,
|
|
183
|
+
gameId,
|
|
184
|
+
gameType,
|
|
185
|
+
potSizeLamports,
|
|
186
|
+
potSizeLamports, // Using pot size as "buy-in" since commission is based on whole pot
|
|
187
|
+
true, // Game was created (considered a "win" for commission purposes)
|
|
188
|
+
0,
|
|
189
|
+
REFERRAL_COMMISSION_RATE,
|
|
190
|
+
commissionLamports
|
|
191
|
+
]);
|
|
192
|
+
|
|
193
|
+
if (insertResult.rows.length > 0) {
|
|
194
|
+
summary.totalCommissionsLamports = commissionLamports;
|
|
195
|
+
summary.commissions.push({
|
|
196
|
+
referrerWallet: game.referrer_wallet,
|
|
197
|
+
referrerUsername: game.referrer_username,
|
|
198
|
+
refereeWallet: game.creator_wallet,
|
|
199
|
+
refereeUsername: game.creator_username,
|
|
200
|
+
commissionLamports,
|
|
201
|
+
commissionSOL: commissionLamports / LAMPORTS_PER_SOL
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
console.log(`${logPrefix} ✅ SUCCESS! Created referral_earnings record (ID: ${insertResult.rows[0].id})`);
|
|
205
|
+
console.log(`${logPrefix} ${game.referrer_username || game.referrer_wallet.slice(0, 8)} earns ${commissionLamports / LAMPORTS_PER_SOL} SOL`);
|
|
206
|
+
console.log(`${logPrefix} from ${game.creator_username || game.creator_wallet.slice(0, 8)}'s game`);
|
|
207
|
+
} else {
|
|
208
|
+
console.log(`${logPrefix} ℹ️ Record already exists (conflict on referee_wallet + game_id)`);
|
|
209
|
+
console.log(`${logPrefix} This is expected if processGameCommissions was called twice for same game`);
|
|
210
|
+
}
|
|
211
|
+
} catch (insertError) {
|
|
212
|
+
console.error(`${logPrefix} ❌ ERROR inserting referral_earnings record:`);
|
|
213
|
+
console.error(`${logPrefix} Message: ${insertError.message}`);
|
|
214
|
+
console.error(`${logPrefix} Code: ${insertError.code}`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const duration = Date.now() - startTime;
|
|
218
|
+
console.log(`${logPrefix} ════════════════════════════════════════════════════`);
|
|
219
|
+
console.log(`${logPrefix} 🎉 Processed in ${duration}ms`);
|
|
220
|
+
console.log(`${logPrefix} ════════════════════════════════════════════════════`);
|
|
221
|
+
|
|
222
|
+
return summary;
|
|
223
|
+
|
|
224
|
+
} catch (error) {
|
|
225
|
+
console.error(`${logPrefix} ❌ FATAL ERROR processing game commissions:`);
|
|
226
|
+
console.error(`${logPrefix} Message: ${error.message}`);
|
|
227
|
+
console.error(`${logPrefix} Stack: ${error.stack}`);
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Get referral earnings for a specific wallet
|
|
234
|
+
* @param {string} walletAddress - Referrer's wallet address
|
|
235
|
+
* @param {Object} options - Query options
|
|
236
|
+
* @returns {Object} - Earnings list and summary
|
|
237
|
+
*/
|
|
238
|
+
async function getEarningsForWallet(walletAddress, options = {}) {
|
|
239
|
+
const { limit = 50, offset = 0, status = null } = options;
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
// Get summary stats
|
|
243
|
+
const summaryResult = await pool.query(`
|
|
244
|
+
SELECT
|
|
245
|
+
COUNT(*) as total_earnings,
|
|
246
|
+
COALESCE(SUM(commission_amount), 0) as total_earned_lamports,
|
|
247
|
+
COALESCE(SUM(CASE WHEN status = 'pending' THEN commission_amount ELSE 0 END), 0) as pending_lamports,
|
|
248
|
+
COALESCE(SUM(CASE WHEN status = 'paid' THEN commission_amount ELSE 0 END), 0) as paid_lamports,
|
|
249
|
+
COUNT(DISTINCT referee_wallet) as unique_referees,
|
|
250
|
+
COUNT(DISTINCT game_id) as games_count
|
|
251
|
+
FROM referral_earnings
|
|
252
|
+
WHERE referrer_wallet = $1
|
|
253
|
+
`, [walletAddress]);
|
|
254
|
+
|
|
255
|
+
// Build query for individual earnings
|
|
256
|
+
let earningsQuery = `
|
|
257
|
+
SELECT
|
|
258
|
+
re.*,
|
|
259
|
+
u.username as referee_username,
|
|
260
|
+
u.avatar as referee_avatar,
|
|
261
|
+
g.title as game_title,
|
|
262
|
+
g.sports_event
|
|
263
|
+
FROM referral_earnings re
|
|
264
|
+
LEFT JOIN users u ON re.referee_wallet = u.wallet_address
|
|
265
|
+
LEFT JOIN games g ON re.game_id = g.game_id
|
|
266
|
+
WHERE re.referrer_wallet = $1
|
|
267
|
+
`;
|
|
268
|
+
const params = [walletAddress];
|
|
269
|
+
|
|
270
|
+
if (status) {
|
|
271
|
+
earningsQuery += ` AND re.status = $${params.length + 1}`;
|
|
272
|
+
params.push(status);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
earningsQuery += ` ORDER BY re.created_at DESC LIMIT $${params.length + 1} OFFSET $${params.length + 2}`;
|
|
276
|
+
params.push(limit, offset);
|
|
277
|
+
|
|
278
|
+
const earningsResult = await pool.query(earningsQuery, params);
|
|
279
|
+
|
|
280
|
+
const summary = summaryResult.rows[0];
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
summary: {
|
|
284
|
+
totalEarnings: parseInt(summary.total_earnings),
|
|
285
|
+
totalEarnedLamports: parseInt(summary.total_earned_lamports),
|
|
286
|
+
totalEarnedSOL: parseInt(summary.total_earned_lamports) / LAMPORTS_PER_SOL,
|
|
287
|
+
pendingLamports: parseInt(summary.pending_lamports),
|
|
288
|
+
pendingSOL: parseInt(summary.pending_lamports) / LAMPORTS_PER_SOL,
|
|
289
|
+
paidLamports: parseInt(summary.paid_lamports),
|
|
290
|
+
paidSOL: parseInt(summary.paid_lamports) / LAMPORTS_PER_SOL,
|
|
291
|
+
uniqueReferees: parseInt(summary.unique_referees),
|
|
292
|
+
gamesCount: parseInt(summary.games_count)
|
|
293
|
+
},
|
|
294
|
+
earnings: earningsResult.rows.map(row => ({
|
|
295
|
+
id: row.id,
|
|
296
|
+
refereeWallet: row.referee_wallet,
|
|
297
|
+
refereeUsername: row.referee_username,
|
|
298
|
+
refereeAvatar: row.referee_avatar,
|
|
299
|
+
gameId: row.game_id,
|
|
300
|
+
gameType: row.game_type,
|
|
301
|
+
gameTitle: row.game_title,
|
|
302
|
+
sportsEvent: row.sports_event,
|
|
303
|
+
refereeBuyInLamports: parseInt(row.referee_buy_in),
|
|
304
|
+
refereeBuyInSOL: parseInt(row.referee_buy_in) / LAMPORTS_PER_SOL,
|
|
305
|
+
refereeWon: row.referee_won,
|
|
306
|
+
refereePayoutLamports: parseInt(row.referee_payout),
|
|
307
|
+
refereePayoutSOL: parseInt(row.referee_payout) / LAMPORTS_PER_SOL,
|
|
308
|
+
commissionRate: parseFloat(row.commission_rate),
|
|
309
|
+
commissionLamports: parseInt(row.commission_amount),
|
|
310
|
+
commissionSOL: parseInt(row.commission_amount) / LAMPORTS_PER_SOL,
|
|
311
|
+
status: row.status,
|
|
312
|
+
createdAt: row.created_at,
|
|
313
|
+
paidAt: row.paid_at,
|
|
314
|
+
txSignature: row.payout_tx_signature // Include tx signature for transaction matching
|
|
315
|
+
})),
|
|
316
|
+
pagination: {
|
|
317
|
+
limit,
|
|
318
|
+
offset,
|
|
319
|
+
hasMore: earningsResult.rows.length === limit
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
} catch (error) {
|
|
324
|
+
console.error('[ReferralEarnings] Error getting earnings:', error.message);
|
|
325
|
+
throw error;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Get pending payouts grouped by referrer (for batch payout processing)
|
|
331
|
+
* @param {number} minAmount - Minimum amount in lamports to include
|
|
332
|
+
* @returns {Array} - List of referrers with pending amounts
|
|
333
|
+
*/
|
|
334
|
+
async function getPendingPayouts(minAmount = 0) {
|
|
335
|
+
try {
|
|
336
|
+
const result = await pool.query(`
|
|
337
|
+
SELECT
|
|
338
|
+
referrer_wallet,
|
|
339
|
+
COUNT(*) as num_earnings,
|
|
340
|
+
SUM(commission_amount) as total_pending_lamports
|
|
341
|
+
FROM referral_earnings
|
|
342
|
+
WHERE status = 'pending'
|
|
343
|
+
GROUP BY referrer_wallet
|
|
344
|
+
HAVING SUM(commission_amount) >= $1
|
|
345
|
+
ORDER BY SUM(commission_amount) DESC
|
|
346
|
+
`, [minAmount]);
|
|
347
|
+
|
|
348
|
+
return result.rows.map(row => ({
|
|
349
|
+
referrerWallet: row.referrer_wallet,
|
|
350
|
+
numEarnings: parseInt(row.num_earnings),
|
|
351
|
+
totalPendingLamports: parseInt(row.total_pending_lamports),
|
|
352
|
+
totalPendingSOL: parseInt(row.total_pending_lamports) / LAMPORTS_PER_SOL
|
|
353
|
+
}));
|
|
354
|
+
|
|
355
|
+
} catch (error) {
|
|
356
|
+
console.error('[ReferralEarnings] Error getting pending payouts:', error.message);
|
|
357
|
+
throw error;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Mark earnings as paid (called after successful payout transaction)
|
|
363
|
+
* @param {string} referrerWallet - Wallet that received payout
|
|
364
|
+
* @param {string} txSignature - Solana transaction signature
|
|
365
|
+
* @param {number} batchId - Optional batch ID
|
|
366
|
+
* @returns {number} - Number of records updated
|
|
367
|
+
*/
|
|
368
|
+
async function markEarningsAsPaid(referrerWallet, txSignature, batchId = null) {
|
|
369
|
+
try {
|
|
370
|
+
const result = await pool.query(`
|
|
371
|
+
UPDATE referral_earnings
|
|
372
|
+
SET
|
|
373
|
+
status = 'paid',
|
|
374
|
+
paid_at = NOW(),
|
|
375
|
+
payout_tx_signature = $2,
|
|
376
|
+
payout_batch_id = $3
|
|
377
|
+
WHERE referrer_wallet = $1
|
|
378
|
+
AND status = 'pending'
|
|
379
|
+
RETURNING id
|
|
380
|
+
`, [referrerWallet, txSignature, batchId]);
|
|
381
|
+
|
|
382
|
+
console.log(`[ReferralEarnings] ✅ Marked ${result.rows.length} earnings as paid for ${referrerWallet.slice(0, 8)}`);
|
|
383
|
+
return result.rows.length;
|
|
384
|
+
|
|
385
|
+
} catch (error) {
|
|
386
|
+
console.error('[ReferralEarnings] Error marking earnings as paid:', error.message);
|
|
387
|
+
throw error;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Create a payout batch record
|
|
393
|
+
* @param {number} totalAmount - Total lamports in batch
|
|
394
|
+
* @param {number} numReferrers - Number of unique referrers
|
|
395
|
+
* @param {number} numEarnings - Number of earning records
|
|
396
|
+
* @returns {number} - Batch ID
|
|
397
|
+
*/
|
|
398
|
+
async function createPayoutBatch(totalAmount, numReferrers, numEarnings) {
|
|
399
|
+
try {
|
|
400
|
+
const result = await pool.query(`
|
|
401
|
+
INSERT INTO referral_payout_batches (
|
|
402
|
+
total_amount,
|
|
403
|
+
num_referrers,
|
|
404
|
+
num_earnings,
|
|
405
|
+
status
|
|
406
|
+
) VALUES ($1, $2, $3, 'pending')
|
|
407
|
+
RETURNING id
|
|
408
|
+
`, [totalAmount, numReferrers, numEarnings]);
|
|
409
|
+
|
|
410
|
+
return result.rows[0].id;
|
|
411
|
+
|
|
412
|
+
} catch (error) {
|
|
413
|
+
console.error('[ReferralEarnings] Error creating payout batch:', error.message);
|
|
414
|
+
throw error;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Update payout batch status
|
|
420
|
+
* @param {number} batchId - Batch ID
|
|
421
|
+
* @param {string} status - New status
|
|
422
|
+
* @param {string} txSignature - Optional transaction signature
|
|
423
|
+
* @param {string} errorMessage - Optional error message
|
|
424
|
+
*/
|
|
425
|
+
async function updatePayoutBatch(batchId, status, txSignature = null, errorMessage = null) {
|
|
426
|
+
try {
|
|
427
|
+
const completedAt = status === 'completed' ? 'NOW()' : 'NULL';
|
|
428
|
+
|
|
429
|
+
await pool.query(`
|
|
430
|
+
UPDATE referral_payout_batches
|
|
431
|
+
SET
|
|
432
|
+
status = $2,
|
|
433
|
+
tx_signature = COALESCE($3, tx_signature),
|
|
434
|
+
error_message = $4,
|
|
435
|
+
processed_at = CASE WHEN $2 = 'processing' THEN NOW() ELSE processed_at END,
|
|
436
|
+
completed_at = CASE WHEN $2 = 'completed' THEN NOW() ELSE completed_at END,
|
|
437
|
+
retry_count = CASE WHEN $2 = 'failed' THEN retry_count + 1 ELSE retry_count END
|
|
438
|
+
WHERE id = $1
|
|
439
|
+
`, [batchId, status, txSignature, errorMessage]);
|
|
440
|
+
|
|
441
|
+
} catch (error) {
|
|
442
|
+
console.error('[ReferralEarnings] Error updating payout batch:', error.message);
|
|
443
|
+
throw error;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Get referral leaderboard (top earners)
|
|
449
|
+
* @param {number} limit - Number of results
|
|
450
|
+
* @returns {Array} - Leaderboard entries
|
|
451
|
+
*/
|
|
452
|
+
async function getLeaderboard(limit = 20) {
|
|
453
|
+
try {
|
|
454
|
+
const result = await pool.query(`
|
|
455
|
+
SELECT
|
|
456
|
+
re.referrer_wallet,
|
|
457
|
+
u.username,
|
|
458
|
+
u.avatar,
|
|
459
|
+
COUNT(DISTINCT re.referee_wallet) as total_referees,
|
|
460
|
+
COUNT(re.id) as total_games,
|
|
461
|
+
SUM(re.commission_amount) as total_earned_lamports
|
|
462
|
+
FROM referral_earnings re
|
|
463
|
+
LEFT JOIN users u ON re.referrer_wallet = u.wallet_address
|
|
464
|
+
GROUP BY re.referrer_wallet, u.username, u.avatar
|
|
465
|
+
ORDER BY SUM(re.commission_amount) DESC
|
|
466
|
+
LIMIT $1
|
|
467
|
+
`, [limit]);
|
|
468
|
+
|
|
469
|
+
return result.rows.map((row, index) => ({
|
|
470
|
+
rank: index + 1,
|
|
471
|
+
walletAddress: row.referrer_wallet,
|
|
472
|
+
username: row.username,
|
|
473
|
+
avatar: row.avatar,
|
|
474
|
+
totalReferees: parseInt(row.total_referees),
|
|
475
|
+
totalGames: parseInt(row.total_games),
|
|
476
|
+
totalEarnedLamports: parseInt(row.total_earned_lamports),
|
|
477
|
+
totalEarnedSOL: parseInt(row.total_earned_lamports) / LAMPORTS_PER_SOL
|
|
478
|
+
}));
|
|
479
|
+
|
|
480
|
+
} catch (error) {
|
|
481
|
+
console.error('[ReferralEarnings] Error getting leaderboard:', error.message);
|
|
482
|
+
throw error;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Get platform-wide referral stats
|
|
488
|
+
* @returns {Object} - Platform stats
|
|
489
|
+
*/
|
|
490
|
+
async function getPlatformStats() {
|
|
491
|
+
try {
|
|
492
|
+
// Get stats from referral_earnings table (actual commissions paid)
|
|
493
|
+
const earningsResult = await pool.query(`
|
|
494
|
+
SELECT
|
|
495
|
+
COUNT(DISTINCT referrer_wallet) as total_referrers_with_earnings,
|
|
496
|
+
COUNT(DISTINCT referee_wallet) as total_referees_with_earnings,
|
|
497
|
+
COUNT(*) as total_earnings,
|
|
498
|
+
COALESCE(SUM(commission_amount), 0) as total_paid_out_lamports,
|
|
499
|
+
COALESCE(SUM(CASE WHEN status = 'pending' THEN commission_amount ELSE 0 END), 0) as total_pending_lamports
|
|
500
|
+
FROM referral_earnings
|
|
501
|
+
`);
|
|
502
|
+
|
|
503
|
+
// Get stats from users table (referral relationships)
|
|
504
|
+
// Active referrers: users who have a my_referral_code AND have at least one user who used their code
|
|
505
|
+
const referrersResult = await pool.query(`
|
|
506
|
+
SELECT COUNT(DISTINCT referrer.id) as total_active_referrers
|
|
507
|
+
FROM users referrer
|
|
508
|
+
WHERE referrer.my_referral_code IS NOT NULL
|
|
509
|
+
AND EXISTS (
|
|
510
|
+
SELECT 1 FROM users referee
|
|
511
|
+
WHERE referee.referral_code = referrer.my_referral_code
|
|
512
|
+
)
|
|
513
|
+
`);
|
|
514
|
+
|
|
515
|
+
// Total referred users: users who signed up with a referral code
|
|
516
|
+
const refereesResult = await pool.query(`
|
|
517
|
+
SELECT COUNT(*) as total_referred_users
|
|
518
|
+
FROM users
|
|
519
|
+
WHERE referral_code IS NOT NULL
|
|
520
|
+
`);
|
|
521
|
+
|
|
522
|
+
const earningsStats = earningsResult.rows[0];
|
|
523
|
+
const referrersStats = referrersResult.rows[0];
|
|
524
|
+
const refereesStats = refereesResult.rows[0];
|
|
525
|
+
|
|
526
|
+
return {
|
|
527
|
+
// Active referrers: users who have referred at least one person (from users table)
|
|
528
|
+
totalReferrers: parseInt(referrersStats.total_active_referrers) || 0,
|
|
529
|
+
// Referred users: users who signed up with a referral code (from users table)
|
|
530
|
+
totalReferees: parseInt(refereesStats.total_referred_users) || 0,
|
|
531
|
+
// Earnings stats (from referral_earnings table)
|
|
532
|
+
totalEarnings: parseInt(earningsStats.total_earnings) || 0,
|
|
533
|
+
totalPaidOutLamports: parseInt(earningsStats.total_paid_out_lamports) || 0,
|
|
534
|
+
totalPaidOutSOL: parseInt(earningsStats.total_paid_out_lamports) / LAMPORTS_PER_SOL || 0,
|
|
535
|
+
totalPendingLamports: parseInt(earningsStats.total_pending_lamports) || 0,
|
|
536
|
+
totalPendingSOL: parseInt(earningsStats.total_pending_lamports) / LAMPORTS_PER_SOL || 0
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
} catch (error) {
|
|
540
|
+
console.error('[ReferralEarnings] Error getting platform stats:', error.message);
|
|
541
|
+
throw error;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Record an on-chain commission payment
|
|
547
|
+
* Called by the oracle after game resolution with referrer
|
|
548
|
+
*
|
|
549
|
+
* This is for tracking/display purposes - the actual SOL transfer
|
|
550
|
+
* already happened on-chain during game resolution.
|
|
551
|
+
*
|
|
552
|
+
* @param {string} gameId - The game ID
|
|
553
|
+
* @param {string} referrerWallet - Referrer's wallet that received payment
|
|
554
|
+
* @param {number} commissionLamports - Commission amount in lamports
|
|
555
|
+
* @param {boolean} paidOnChain - Whether payment was made on-chain (should be true)
|
|
556
|
+
* @param {string} txSignature - Transaction signature for the payout
|
|
557
|
+
* @param {string} gameType - Type of game ('sports', 'connect4', 'billiards')
|
|
558
|
+
* @returns {Object} - Result of the recording
|
|
559
|
+
*/
|
|
560
|
+
async function recordOnChainCommission(gameId, referrerWallet, commissionLamports, paidOnChain = true, txSignature = null, gameType = 'sports') {
|
|
561
|
+
const logPrefix = `[ONCHAIN-COMMISSION:${gameId.slice(-8)}]`;
|
|
562
|
+
|
|
563
|
+
console.log(`${logPrefix} ════════════════════════════════════════════════════`);
|
|
564
|
+
console.log(`${logPrefix} 📝 Recording on-chain referral commission`);
|
|
565
|
+
console.log(`${logPrefix} Full Game ID: ${gameId}`);
|
|
566
|
+
console.log(`${logPrefix} Referrer Wallet: ${referrerWallet}`);
|
|
567
|
+
console.log(`${logPrefix} Commission: ${commissionLamports} lamports (${commissionLamports / LAMPORTS_PER_SOL} SOL)`);
|
|
568
|
+
console.log(`${logPrefix} Paid On-Chain: ${paidOnChain}`);
|
|
569
|
+
console.log(`${logPrefix} TX Signature: ${txSignature || 'NOT PROVIDED ⚠️'}`);
|
|
570
|
+
console.log(`${logPrefix} Game Type: ${gameType}`);
|
|
571
|
+
console.log(`${logPrefix} ════════════════════════════════════════════════════`);
|
|
572
|
+
|
|
573
|
+
if (!txSignature) {
|
|
574
|
+
console.log(`${logPrefix} ⚠️ WARNING: No transaction signature - record will be unverifiable on blockchain!`);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
try {
|
|
578
|
+
// Get game and referrer info
|
|
579
|
+
console.log(`${logPrefix} Step 1: Looking up game and user info...`);
|
|
580
|
+
const gameResult = await pool.query(`
|
|
581
|
+
SELECT
|
|
582
|
+
g.game_id,
|
|
583
|
+
g.created_by as creator_wallet,
|
|
584
|
+
g.title,
|
|
585
|
+
creator.id as creator_user_id,
|
|
586
|
+
creator.username as creator_username,
|
|
587
|
+
referrer.id as referrer_user_id,
|
|
588
|
+
referrer.username as referrer_username
|
|
589
|
+
FROM games g
|
|
590
|
+
JOIN users creator ON g.created_by = creator.wallet_address
|
|
591
|
+
LEFT JOIN users referrer ON referrer.wallet_address = $2
|
|
592
|
+
WHERE g.game_id = $1
|
|
593
|
+
`, [gameId, referrerWallet]);
|
|
594
|
+
|
|
595
|
+
console.log(`${logPrefix} Query returned ${gameResult.rows.length} row(s)`);
|
|
596
|
+
|
|
597
|
+
if (gameResult.rows.length === 0) {
|
|
598
|
+
console.log(`${logPrefix} ❌ Game ${gameId} not found in database`);
|
|
599
|
+
return { success: false, error: 'Game not found' };
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const game = gameResult.rows[0];
|
|
603
|
+
console.log(`${logPrefix} Game: ${game.title || 'untitled'}`);
|
|
604
|
+
console.log(`${logPrefix} Creator: ${game.creator_username || 'unknown'} (${game.creator_wallet})`);
|
|
605
|
+
console.log(`${logPrefix} Referrer user in DB: ${game.referrer_username || 'NOT FOUND'} (ID: ${game.referrer_user_id || 'NULL'})`);
|
|
606
|
+
|
|
607
|
+
// Check if already recorded
|
|
608
|
+
console.log(`${logPrefix} Step 2: Checking for existing referral_earnings record...`);
|
|
609
|
+
const existingResult = await pool.query(`
|
|
610
|
+
SELECT id, status, payout_tx_signature FROM referral_earnings
|
|
611
|
+
WHERE game_id = $1 AND referrer_wallet = $2
|
|
612
|
+
`, [gameId, referrerWallet]);
|
|
613
|
+
|
|
614
|
+
console.log(`${logPrefix} Found ${existingResult.rows.length} existing record(s)`);
|
|
615
|
+
|
|
616
|
+
if (existingResult.rows.length > 0) {
|
|
617
|
+
const existing = existingResult.rows[0];
|
|
618
|
+
console.log(`${logPrefix} Existing record ID: ${existing.id}`);
|
|
619
|
+
console.log(`${logPrefix} Existing status: ${existing.status}`);
|
|
620
|
+
console.log(`${logPrefix} Existing tx_signature: ${existing.payout_tx_signature || 'NULL'}`);
|
|
621
|
+
|
|
622
|
+
// Update existing record to mark as paid on-chain with signature
|
|
623
|
+
console.log(`${logPrefix} Step 3: Updating existing record with paid status and signature...`);
|
|
624
|
+
await pool.query(`
|
|
625
|
+
UPDATE referral_earnings
|
|
626
|
+
SET
|
|
627
|
+
status = 'paid',
|
|
628
|
+
paid_at = NOW(),
|
|
629
|
+
payout_tx_signature = COALESCE($3, payout_tx_signature),
|
|
630
|
+
notes = 'Paid on-chain during game resolution'
|
|
631
|
+
WHERE game_id = $1 AND referrer_wallet = $2
|
|
632
|
+
`, [gameId, referrerWallet, txSignature]);
|
|
633
|
+
|
|
634
|
+
console.log(`${logPrefix} ✅ SUCCESS: Updated existing record as paid on-chain`);
|
|
635
|
+
console.log(`${logPrefix} Record ID: ${existing.id}`);
|
|
636
|
+
console.log(`${logPrefix} New signature: ${txSignature || '(kept existing)'}`);
|
|
637
|
+
|
|
638
|
+
return {
|
|
639
|
+
success: true,
|
|
640
|
+
action: 'updated',
|
|
641
|
+
earningId: existing.id,
|
|
642
|
+
txSignature
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Insert new record (already paid on-chain) with signature
|
|
647
|
+
console.log(`${logPrefix} Step 3: No existing record - creating new one with 'paid' status...`);
|
|
648
|
+
const estimatedPotSize = commissionLamports * 100; // 1% commission means pot is 100x
|
|
649
|
+
console.log(`${logPrefix} Estimated pot size: ${estimatedPotSize} lamports (${estimatedPotSize / LAMPORTS_PER_SOL} SOL)`);
|
|
650
|
+
|
|
651
|
+
const insertResult = await pool.query(`
|
|
652
|
+
INSERT INTO referral_earnings (
|
|
653
|
+
referrer_user_id,
|
|
654
|
+
referrer_wallet,
|
|
655
|
+
referee_user_id,
|
|
656
|
+
referee_wallet,
|
|
657
|
+
game_id,
|
|
658
|
+
game_type,
|
|
659
|
+
pot_size,
|
|
660
|
+
referee_buy_in,
|
|
661
|
+
referee_won,
|
|
662
|
+
referee_payout,
|
|
663
|
+
commission_rate,
|
|
664
|
+
commission_amount,
|
|
665
|
+
status,
|
|
666
|
+
paid_at,
|
|
667
|
+
payout_tx_signature,
|
|
668
|
+
notes
|
|
669
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $7, true, 0, $8, $9, 'paid', NOW(), $10, 'Paid on-chain during game resolution')
|
|
670
|
+
RETURNING id
|
|
671
|
+
`, [
|
|
672
|
+
game.referrer_user_id || null,
|
|
673
|
+
referrerWallet,
|
|
674
|
+
game.creator_user_id,
|
|
675
|
+
game.creator_wallet,
|
|
676
|
+
gameId,
|
|
677
|
+
gameType, // Use the provided gameType
|
|
678
|
+
estimatedPotSize, // pot_size (estimated from commission)
|
|
679
|
+
REFERRAL_COMMISSION_RATE,
|
|
680
|
+
commissionLamports,
|
|
681
|
+
txSignature
|
|
682
|
+
]);
|
|
683
|
+
|
|
684
|
+
console.log(`${logPrefix} ✅ SUCCESS: Created new referral_earnings record`);
|
|
685
|
+
console.log(`${logPrefix} New Record ID: ${insertResult.rows[0].id}`);
|
|
686
|
+
console.log(`${logPrefix} TX Signature stored: ${txSignature || 'NULL'}`);
|
|
687
|
+
console.log(`${logPrefix} ════════════════════════════════════════════════════`);
|
|
688
|
+
|
|
689
|
+
return {
|
|
690
|
+
success: true,
|
|
691
|
+
action: 'created',
|
|
692
|
+
earningId: insertResult.rows[0].id,
|
|
693
|
+
txSignature
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
} catch (error) {
|
|
697
|
+
console.error(`${logPrefix} ❌ FATAL ERROR recording on-chain commission:`);
|
|
698
|
+
console.error(`${logPrefix} Message: ${error.message}`);
|
|
699
|
+
console.error(`${logPrefix} Code: ${error.code}`);
|
|
700
|
+
console.error(`${logPrefix} Stack: ${error.stack}`);
|
|
701
|
+
return { success: false, error: error.message };
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
module.exports = {
|
|
706
|
+
// Configuration
|
|
707
|
+
REFERRAL_COMMISSION_RATE,
|
|
708
|
+
LAMPORTS_PER_SOL,
|
|
709
|
+
|
|
710
|
+
// Core functions
|
|
711
|
+
getReferrerForWallet,
|
|
712
|
+
processGameCommissions,
|
|
713
|
+
getEarningsForWallet,
|
|
714
|
+
|
|
715
|
+
// On-chain commission recording
|
|
716
|
+
recordOnChainCommission,
|
|
717
|
+
|
|
718
|
+
// Payout functions (for off-chain batch payouts - legacy)
|
|
719
|
+
getPendingPayouts,
|
|
720
|
+
markEarningsAsPaid,
|
|
721
|
+
createPayoutBatch,
|
|
722
|
+
updatePayoutBatch,
|
|
723
|
+
|
|
724
|
+
// Stats and leaderboard
|
|
725
|
+
getLeaderboard,
|
|
726
|
+
getPlatformStats
|
|
727
|
+
};
|
|
728
|
+
|