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.
Files changed (304) hide show
  1. package/.claude/settings.local.json +280 -0
  2. package/CLAUDE.md +46 -0
  3. package/CONNECT4_PRODUCTION_DEPLOY.md +155 -0
  4. package/CURRENT_SESSION.md +171 -0
  5. package/CURRENT_SESSION_DRAW.md +516 -0
  6. package/MARCH_MADNESS_SURVIVOR.md +254 -0
  7. package/PANDA.md +166 -0
  8. package/Procfile +4 -0
  9. package/README.md +476 -0
  10. package/controllers/livescoresController.js +376 -0
  11. package/controllers/pickemController.js +554 -0
  12. package/controllers/survivorAdminController.js +887 -0
  13. package/controllers/survivorController.js +623 -0
  14. package/cron/oracleMonitor.js +77 -0
  15. package/cron/pickemOracleMonitor.js +73 -0
  16. package/data/jackpot-history.json +952 -0
  17. package/data/ncaaTeams.js +406 -0
  18. package/documentation/API_SECURITY_GUIDE.md +327 -0
  19. package/documentation/ARCADE_API.md +593 -0
  20. package/documentation/ARCADE_IMPLEMENTATION_SUMMARY.md +399 -0
  21. package/documentation/ARCADE_QUICKSTART.md +242 -0
  22. package/documentation/AUTOMATIC_MODE_ORACLE.md +321 -0
  23. package/documentation/BUG_FIX_COHORT_DATE_DISPLAY.md +171 -0
  24. package/documentation/CLAIM_MIGRATION_INSTRUCTIONS.md +52 -0
  25. package/documentation/CLAIM_STATUS_FIX.md +67 -0
  26. package/documentation/CLI_TOOL_GUIDE.md +372 -0
  27. package/documentation/COHORT_RETENTION_ANALYSIS.md +295 -0
  28. package/documentation/COHORT_RETENTION_IMPLEMENTATION_COMPLETE.md +461 -0
  29. package/documentation/COHORT_RETENTION_SUMMARY.md +204 -0
  30. package/documentation/COMPLETE_PROJECT_SUMMARY.md +490 -0
  31. package/documentation/DATABASE_QUERIES.md +269 -0
  32. package/documentation/DATABASE_RETENTION_POLICY.md +390 -0
  33. package/documentation/DATABASE_SETUP_GUIDE.md +361 -0
  34. package/documentation/DATABASE_SETUP_SUMMARY.md +247 -0
  35. package/documentation/DEMO_API_CURL_COMMANDS.md +656 -0
  36. package/documentation/DEPLOYMENT_SUMMARY.txt +100 -0
  37. package/documentation/DUPLICATE_NOTIFICATIONS_FIXED.md +201 -0
  38. package/documentation/EXCHANGE_RATES_INTEGRATION.md +371 -0
  39. package/documentation/FINAL_API_PROTECTION_TABLE.md +175 -0
  40. package/documentation/GAME_START_NOTIFICATIONS_DEPLOYMENT.md +256 -0
  41. package/documentation/GAME_START_NOTIFICATIONS_INTEGRATION.md +275 -0
  42. package/documentation/HEROKU_DEPLOYMENT.md +134 -0
  43. package/documentation/HEROKU_SCHEDULER_SETUP.md +271 -0
  44. package/documentation/JACKPOT_API.md +521 -0
  45. package/documentation/JACKPOT_DEPLOYMENT_GUIDE.md +362 -0
  46. package/documentation/JWT_IMPLEMENTATION_SUMMARY.md +373 -0
  47. package/documentation/JWT_QUICK_SETUP.md +268 -0
  48. package/documentation/JWT_TESTING_GUIDE.md +404 -0
  49. package/documentation/KEEPER_RECOVERY_GUIDE.md +381 -0
  50. package/documentation/KEEPER_SETUP.md +206 -0
  51. package/documentation/KEEPER_STATE_MACHINE.md +423 -0
  52. package/documentation/LATEST_PRODUCTION_SETUP.md +387 -0
  53. package/documentation/LOCAL_VOTING_TEST.md +279 -0
  54. package/documentation/ORACLE_FIXES_SUMMARY.md +188 -0
  55. package/documentation/ORACLE_POSTGRESQL_UPDATE.md +202 -0
  56. package/documentation/PAYMENT_DEPLOYMENT.md +209 -0
  57. package/documentation/PNL_TRACKING_SETUP.md +189 -0
  58. package/documentation/PREVENTING_LOCKUP_ERRORS.md +472 -0
  59. package/documentation/PRODUCTION_READY_SUMMARY.md +227 -0
  60. package/documentation/PUBLIC_VS_PRIVATE_ENDPOINTS.md +278 -0
  61. package/documentation/QUICK_AUTH_SETUP.md +99 -0
  62. package/documentation/QUICK_DEPLOY.md +224 -0
  63. package/documentation/QUICK_FIX.md +114 -0
  64. package/documentation/QUICK_START.md +152 -0
  65. package/documentation/REFEREE_MODE_GUIDE.md +392 -0
  66. package/documentation/RETENTION_CORE_ACTION_UPDATE.md +313 -0
  67. package/documentation/RETENTION_UPDATE_SUMMARY.md +108 -0
  68. package/documentation/RUN_MIGRATION_NOW.md +39 -0
  69. package/documentation/SCRIPTS_UPDATE_SUMMARY.md +251 -0
  70. package/documentation/SETUP_GUIDE.md +184 -0
  71. package/documentation/STATE_MACHINE_IMPLEMENTATION.md +250 -0
  72. package/documentation/TELEGRAM_NOTIFICATIONS_DIAGNOSIS.md +361 -0
  73. package/documentation/UNIFIED_ARCHITECTURE.md +231 -0
  74. package/documentation/VOTING_DEPLOYMENT_SUMMARY.md +392 -0
  75. package/documentation/WEBSOCKET_ARCHITECTURE.md +881 -0
  76. package/documentation/WHAT_WE_BUILT_TODAY.md +369 -0
  77. package/documentation/latest/LATEST_PRODUCTION_SETUP.md +865 -0
  78. package/ecosystem.config.js +65 -0
  79. package/env.template +125 -0
  80. package/middleware/apiKeyAuth.js +136 -0
  81. package/middleware/authenticate.js +214 -0
  82. package/middleware/developerUserAuth.js +76 -0
  83. package/middleware/socketAuth.js +69 -0
  84. package/package.json +49 -0
  85. package/postman/Dubs-API-v1-With-Voting.postman_collection.json +555 -0
  86. package/postman/Dubs-API-v1.postman_collection.json +205 -0
  87. package/postman/Dubs_Developer_API.postman_collection.json +662 -0
  88. package/postman/QUICKSTART.md +118 -0
  89. package/postman/QUICK_REFERENCE.md +246 -0
  90. package/postman/README.md +71 -0
  91. package/postman/VOTING_API_GUIDE.md +426 -0
  92. package/refactor/Animations.md +148 -0
  93. package/refactor/Chat.md +252 -0
  94. package/routes/actionsRoutes.js +699 -0
  95. package/routes/adminRoutes.js +370 -0
  96. package/routes/analyticsRoutes.js +1262 -0
  97. package/routes/arcadeRoutes.js +557 -0
  98. package/routes/authRoutes.js +2310 -0
  99. package/routes/avatarRoutes.js +85 -0
  100. package/routes/botRoutes.js +211 -0
  101. package/routes/chatRoutes.js +377 -0
  102. package/routes/cryptoPriceRoutes.js +105 -0
  103. package/routes/developerRoutes.js +4201 -0
  104. package/routes/deviceRoutes.js +214 -0
  105. package/routes/dmRoutes.js +167 -0
  106. package/routes/esportsRoutes.js +806 -0
  107. package/routes/exchangeRateRoutes.js +233 -0
  108. package/routes/gamesRoutes.js +3028 -0
  109. package/routes/jackpotRoutes.js +754 -0
  110. package/routes/keeperMonitoringRoutes.js +156 -0
  111. package/routes/keeperWebhookRoutes.js +466 -0
  112. package/routes/livescoresRoutes.js +31 -0
  113. package/routes/pickemAdminRoutes.js +199 -0
  114. package/routes/pickemRoutes.js +231 -0
  115. package/routes/playerStatsRoutes.js +147 -0
  116. package/routes/portfolioRoutes.js +217 -0
  117. package/routes/promoRoutes.js +418 -0
  118. package/routes/referralEarningsRoutes.js +392 -0
  119. package/routes/socialRoutes.js +459 -0
  120. package/routes/sportsRoutes.js +1271 -0
  121. package/routes/survivorAdminRoutes.js +345 -0
  122. package/routes/survivorRoutes.js +756 -0
  123. package/routes/uploadRoutes.js +256 -0
  124. package/routes/userProfileRoutes.js +244 -0
  125. package/routes/whatsNewRoutes.js +331 -0
  126. package/scripts/.claude/settings.local.json +15 -0
  127. package/scripts/README.md +170 -0
  128. package/scripts/RESTART_EVERYTHING.sh +104 -0
  129. package/scripts/add-claim-columns.sql +48 -0
  130. package/scripts/add-crypto-prices-cache.sql +27 -0
  131. package/scripts/add-exchange-rates-cache.sql +40 -0
  132. package/scripts/add-game-invite-column.sql +23 -0
  133. package/scripts/add-game-invite-notification.sql +33 -0
  134. package/scripts/add-game-invite-telegram-pref.sql +16 -0
  135. package/scripts/add-game-joined-notification.sql +16 -0
  136. package/scripts/add-game-joined-pref.js +40 -0
  137. package/scripts/add-game-joined-preference.sql +6 -0
  138. package/scripts/add-game-start-notifications.sql +41 -0
  139. package/scripts/add-notification-flags-to-games.sql +55 -0
  140. package/scripts/add-pending-game-dismissals.sql +19 -0
  141. package/scripts/add-preferred-currency.sql +34 -0
  142. package/scripts/add-winner-columns.js +61 -0
  143. package/scripts/add_mention_system.sql +53 -0
  144. package/scripts/add_payment_system.sql +96 -0
  145. package/scripts/add_sports_event_id_column.sql +22 -0
  146. package/scripts/analyze-cohort-data-heroku.js +276 -0
  147. package/scripts/analyze-cohort-data.js +295 -0
  148. package/scripts/analyze-prod-cohorts.sh +10 -0
  149. package/scripts/backfill-matchup-images.js +245 -0
  150. package/scripts/backfill-missing-signatures.js +175 -0
  151. package/scripts/backfill-referral-earnings.js +202 -0
  152. package/scripts/check-chat-schema.js +130 -0
  153. package/scripts/check-db.sh +14 -0
  154. package/scripts/check_oracle_in_game.js +54 -0
  155. package/scripts/cleanup-database.js +193 -0
  156. package/scripts/clear-notification-cache.js +85 -0
  157. package/scripts/convert-mnemonic.js +50 -0
  158. package/scripts/create-users-table.sql +44 -0
  159. package/scripts/debug-cohort-counts.js +248 -0
  160. package/scripts/debug-winner-calc.js +84 -0
  161. package/scripts/deploy-payment-system.sh +118 -0
  162. package/scripts/deploy-to-heroku.sh +63 -0
  163. package/scripts/diagnose-locked-round.js +143 -0
  164. package/scripts/dubs-cli.js +720 -0
  165. package/scripts/dump-account.js +65 -0
  166. package/scripts/find-vrf-offset.js +48 -0
  167. package/scripts/fix-chat-notifications-constraint.sql +122 -0
  168. package/scripts/fix-claim-columns.js +124 -0
  169. package/scripts/fix-constraint-now.js +44 -0
  170. package/scripts/fix-lock-timestamps.js +96 -0
  171. package/scripts/fix-locked-round.sh +126 -0
  172. package/scripts/fix-missing-badges.sql +91 -0
  173. package/scripts/fix-payment-notifications.sql +41 -0
  174. package/scripts/force-new-round.js +55 -0
  175. package/scripts/force-resolve-and-claim.js +278 -0
  176. package/scripts/important/README.md +115 -0
  177. package/scripts/important/authority-force-lock.js +197 -0
  178. package/scripts/important/authority-resolve-game.js +267 -0
  179. package/scripts/important/check-game-status.js +373 -0
  180. package/scripts/important/list-pending-games-by-version.js +270 -0
  181. package/scripts/important/reconcile-v1-v2-payouts.js +270 -0
  182. package/scripts/initialize-jackpot.js +111 -0
  183. package/scripts/jackpot/.claude/settings.local.json +10 -0
  184. package/scripts/jackpot/force-reset.js +84 -0
  185. package/scripts/jackpot/initialize-mainnet.js +100 -0
  186. package/scripts/jackpot/keeper.js +742 -0
  187. package/scripts/jackpot/status.js +107 -0
  188. package/scripts/jackpot/update-round-duration.js +143 -0
  189. package/scripts/keeper-bot.js +112 -0
  190. package/scripts/list-pending-games.js +131 -0
  191. package/scripts/migrate-chat-v2.js +127 -0
  192. package/scripts/migrate-chat-winners.js +84 -0
  193. package/scripts/migrate-chat.sh +17 -0
  194. package/scripts/migrate-game-invite.js +83 -0
  195. package/scripts/migrate-heroku-game-notifications.sh +159 -0
  196. package/scripts/migrations/001_analytics_tables.sql +422 -0
  197. package/scripts/migrations/002_add_matchup_image_url.sql +14 -0
  198. package/scripts/migrations/003_referral_earnings.sql +208 -0
  199. package/scripts/migrations/004_add_whats_new_notification_type.sql +62 -0
  200. package/scripts/migrations/005_add_connect4_your_turn_notification.sql +61 -0
  201. package/scripts/migrations/005_push_notifications.sql +55 -0
  202. package/scripts/migrations/006_add_draw_team_players.sql +28 -0
  203. package/scripts/migrations/006_add_game_cancelled_notification.sql +62 -0
  204. package/scripts/migrations/007_add_gif_url.sql +8 -0
  205. package/scripts/migrations/008_add_connect4_columns.sql +139 -0
  206. package/scripts/migrations/008_add_pool_tracking.sql +22 -0
  207. package/scripts/migrations/009_create_survivor_pool_tables.sql +174 -0
  208. package/scripts/migrations/010_add_survivor_pool_outcome.sql +28 -0
  209. package/scripts/migrations/011_create_developer_tables.sql +67 -0
  210. package/scripts/migrations/011_fix_keeper_tables.sql +85 -0
  211. package/scripts/migrations/012_create_developer_webhooks.sql +31 -0
  212. package/scripts/migrations/013_add_network_mode.sql +18 -0
  213. package/scripts/migrations/014_create_developer_app_users.sql +19 -0
  214. package/scripts/migrations/015_add_ui_config.sql +4 -0
  215. package/scripts/migrations/016_add_resolution_secret.sql +4 -0
  216. package/scripts/migrations/017_add_external_game_id.sql +3 -0
  217. package/scripts/migrations/018_create_pickem_tables.sql +115 -0
  218. package/scripts/migrations/019_expo_push_tokens.sql +19 -0
  219. package/scripts/migrations/create_whats_new_tables.sql +88 -0
  220. package/scripts/migrations/drop_live_games_tables.sql +34 -0
  221. package/scripts/open-jackpot-round.js +85 -0
  222. package/scripts/purge-all-data.sh +329 -0
  223. package/scripts/purge-all-data.sql +142 -0
  224. package/scripts/purge-heroku-data.sh +149 -0
  225. package/scripts/purge-heroku-data.sql +62 -0
  226. package/scripts/rebuild-heroku-database.sh +113 -0
  227. package/scripts/recover-funds.js +357 -0
  228. package/scripts/regenerate-epl-images.js +278 -0
  229. package/scripts/resize-s3-matchup-images.js +374 -0
  230. package/scripts/resolve-direct.js +88 -0
  231. package/scripts/resolve-mock-game.js +124 -0
  232. package/scripts/resolve-pickem-game.js +55 -0
  233. package/scripts/resolve-round-manual.js +83 -0
  234. package/scripts/resolve-stuck-game.js +382 -0
  235. package/scripts/resolve-stuck-round.js +42 -0
  236. package/scripts/run-connect4-migration.sh +16 -0
  237. package/scripts/run-mention-migration.sh +32 -0
  238. package/scripts/run-payment-migration.sh +51 -0
  239. package/scripts/run-preferred-currency-migration.sh +31 -0
  240. package/scripts/run-referral-earnings-migration.sh +32 -0
  241. package/scripts/run-survivor-outcome-migration.sh +16 -0
  242. package/scripts/seed-test-users.js +346 -0
  243. package/scripts/setup-auth-tables.js +78 -0
  244. package/scripts/setup-complete-database.sql +992 -0
  245. package/scripts/setup-database-fresh.sh +359 -0
  246. package/scripts/setup-heroku-keeper.sh +48 -0
  247. package/scripts/setup-keeper-database.js +83 -0
  248. package/scripts/setup-keeper-state-db.sql +110 -0
  249. package/scripts/setup-oracle.sh +39 -0
  250. package/scripts/setup-pnl-tracking.js +111 -0
  251. package/scripts/start-devnet.sh +14 -0
  252. package/scripts/test-arcade-devnet.sh +160 -0
  253. package/scripts/test-arcade-match.sh +109 -0
  254. package/scripts/test-automatic-mode.sh +239 -0
  255. package/scripts/test-connect4-cancel-claim.js +370 -0
  256. package/scripts/test-connect4-e2e.js +369 -0
  257. package/scripts/test-connect4-resolve.js +369 -0
  258. package/scripts/test-game-state-endpoint.js +136 -0
  259. package/scripts/test-invite-notification.js +86 -0
  260. package/scripts/test-jackpot-api.sh +71 -0
  261. package/scripts/test-poll-confirmation.js +267 -0
  262. package/scripts/test-resolve-game.js +271 -0
  263. package/scripts/test-resolve-signature.js +223 -0
  264. package/scripts/test-signature-preservation.js +124 -0
  265. package/scripts/test-state-machine.js +291 -0
  266. package/scripts/test-webhook-receiver.js +60 -0
  267. package/scripts/update-notification-constraint.js +52 -0
  268. package/scripts/verify-account-layout.js +145 -0
  269. package/scripts/verify-winner-algorithm.js +278 -0
  270. package/server.js +5259 -0
  271. package/services/arcadeMatchService.js +763 -0
  272. package/services/automaticGameOracle.js +1596 -0
  273. package/services/chatService.js +1612 -0
  274. package/services/connect4GameService.js +1049 -0
  275. package/services/connect4NotificationService.js +374 -0
  276. package/services/cryptoPriceService.js +223 -0
  277. package/services/customGameResolver.js +260 -0
  278. package/services/db.js +79 -0
  279. package/services/directMessageService.js +389 -0
  280. package/services/discordNotifications.js +160 -0
  281. package/services/exchangeRateService.js +289 -0
  282. package/services/expoPushService.js +314 -0
  283. package/services/gamesCacheService.js +539 -0
  284. package/services/jackpotHistory.js +331 -0
  285. package/services/jackpotService.js +856 -0
  286. package/services/keeperStateService.js +355 -0
  287. package/services/matchupImageService.js +591 -0
  288. package/services/notificationCacheService.js +407 -0
  289. package/services/pickemOracle.js +440 -0
  290. package/services/playerStatsService.js +389 -0
  291. package/services/portfolioService.js +555 -0
  292. package/services/promoService.js +757 -0
  293. package/services/promoTreasuryService.js +239 -0
  294. package/services/pushNotifications.js +353 -0
  295. package/services/redisService.js +422 -0
  296. package/services/referralEarningsService.js +728 -0
  297. package/services/s3Service.js +396 -0
  298. package/services/socialService.js +1202 -0
  299. package/services/survivorOracle.js +469 -0
  300. package/services/survivorSimulator.js +475 -0
  301. package/services/telegramNotifications.js +461 -0
  302. package/services/userProfileStatsService.js +1185 -0
  303. package/services/whatsNewService.js +388 -0
  304. 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
+