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,1049 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connect 4 Game Service
|
|
3
|
+
*
|
|
4
|
+
* Handles all game logic for Connect 4:
|
|
5
|
+
* - Board state management
|
|
6
|
+
* - Move validation
|
|
7
|
+
* - Win detection
|
|
8
|
+
* - Game state persistence
|
|
9
|
+
* - On-chain payout resolution
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { pool } = require('./db');
|
|
13
|
+
const { Connection, Keypair, PublicKey, Transaction, TransactionInstruction } = require('@solana/web3.js');
|
|
14
|
+
const crypto = require('crypto');
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const referralEarningsService = require('./referralEarningsService');
|
|
18
|
+
|
|
19
|
+
// Solana configuration
|
|
20
|
+
const PROGRAM_ID = new PublicKey(process.env.PROGRAM_ID || 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v');
|
|
21
|
+
const OPERATOR_WALLET = new PublicKey(process.env.OPERATOR_WALLET || 'BVZXwZpfgyzTBdRFHohkHZppPHnAyqyctRsKy3vWfQib');
|
|
22
|
+
const RPC_URL = process.env.SOLANA_RPC_URL; // Must be set to Alchemy URL
|
|
23
|
+
|
|
24
|
+
// Discriminator for resolve_automatic_game instruction
|
|
25
|
+
const RESOLVE_AUTO_DISCRIMINATOR = Buffer.from([245, 33, 115, 150, 82, 150, 28, 193]);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Poll for transaction confirmation using getSignatureStatuses
|
|
29
|
+
* This is the Alchemy-compatible approach (no WebSocket subscriptions)
|
|
30
|
+
*
|
|
31
|
+
* @param {Connection} connection - Solana connection
|
|
32
|
+
* @param {string} signature - Transaction signature to confirm
|
|
33
|
+
* @param {number} lastValidBlockHeight - Block height after which tx expires
|
|
34
|
+
* @param {number} timeout - Maximum time to wait in ms (default 30s)
|
|
35
|
+
* @returns {object} - Confirmation status
|
|
36
|
+
*/
|
|
37
|
+
async function pollTransactionConfirmation(connection, signature, lastValidBlockHeight, timeout = 30000) {
|
|
38
|
+
const start = Date.now();
|
|
39
|
+
console.log(`🔴🟡 [Connect4] Polling confirmation for ${signature}, lastValidBlockHeight: ${lastValidBlockHeight}`);
|
|
40
|
+
|
|
41
|
+
while (Date.now() - start < timeout) {
|
|
42
|
+
try {
|
|
43
|
+
// Check if blockhash has expired (transaction dropped)
|
|
44
|
+
const currentBlockHeight = await connection.getBlockHeight('confirmed');
|
|
45
|
+
if (currentBlockHeight > lastValidBlockHeight) {
|
|
46
|
+
throw new Error(`Transaction expired: blockhash no longer valid (current: ${currentBlockHeight}, lastValid: ${lastValidBlockHeight})`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Poll signature status
|
|
50
|
+
const response = await connection.getSignatureStatuses([signature], {
|
|
51
|
+
searchTransactionHistory: true
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const status = response?.value?.[0];
|
|
55
|
+
if (status) {
|
|
56
|
+
console.log(`🔴🟡 [Connect4] Signature status: ${JSON.stringify(status)}`);
|
|
57
|
+
|
|
58
|
+
// Check for error
|
|
59
|
+
if (status.err) {
|
|
60
|
+
throw new Error(`Transaction failed: ${JSON.stringify(status.err)}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Check confirmation level
|
|
64
|
+
if (status.confirmationStatus === 'confirmed' || status.confirmationStatus === 'finalized') {
|
|
65
|
+
console.log(`✅ [Connect4] Transaction confirmed: ${status.confirmationStatus}`);
|
|
66
|
+
return status;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
} catch (pollErr) {
|
|
70
|
+
// If it's a fatal error (not just "not found yet"), rethrow
|
|
71
|
+
if (pollErr.message?.includes('expired') || pollErr.message?.includes('failed')) {
|
|
72
|
+
throw pollErr;
|
|
73
|
+
}
|
|
74
|
+
console.warn(`⚠️ [Connect4] Poll error (will retry): ${pollErr.message}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Wait before next poll (1 second as recommended by Solana docs)
|
|
78
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
throw new Error(`Transaction confirmation timeout after ${timeout}ms`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Load oracle keypair for Connect4 resolution
|
|
86
|
+
* Connect4 games use automatic mode, which means the smart contract sets
|
|
87
|
+
* game.oracle = hardcoded ORACLE_WALLET (FWUJCthDfPcgmTvdQWM5uofxxiYjqJFMMwiLYvS7LBFa)
|
|
88
|
+
* We MUST use the oracle wallet to resolve, not the operator wallet.
|
|
89
|
+
*/
|
|
90
|
+
function loadOracleKeypair() {
|
|
91
|
+
try {
|
|
92
|
+
// First try environment variable (JSON array of secret key bytes)
|
|
93
|
+
if (process.env.ORACLE_WALLET_JSON) {
|
|
94
|
+
const secretKey = JSON.parse(process.env.ORACLE_WALLET_JSON);
|
|
95
|
+
return Keypair.fromSecretKey(Uint8Array.from(secretKey));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Fallback to file - use oracle.json (matches smart contract hardcoded ORACLE_WALLET)
|
|
99
|
+
const walletPath = process.env.ORACLE_WALLET_PATH || path.join(__dirname, '../wallets/oracle.json');
|
|
100
|
+
if (fs.existsSync(walletPath)) {
|
|
101
|
+
const secretKey = JSON.parse(fs.readFileSync(walletPath, 'utf-8'));
|
|
102
|
+
return Keypair.fromSecretKey(Uint8Array.from(secretKey));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
console.warn('⚠️ [Connect4] No oracle keypair found - payouts will be disabled');
|
|
106
|
+
return null;
|
|
107
|
+
} catch (error) {
|
|
108
|
+
console.error('❌ [Connect4] Failed to load oracle keypair:', error.message);
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Initialize oracle keypair and connection
|
|
114
|
+
let oracleKeypair = null;
|
|
115
|
+
let connection = null;
|
|
116
|
+
|
|
117
|
+
function initializeSolana() {
|
|
118
|
+
console.log(`🔴🟡 [Connect4] initializeSolana called, oracleKeypair exists: ${!!oracleKeypair}`);
|
|
119
|
+
if (!oracleKeypair) {
|
|
120
|
+
console.log(`🔴🟡 [Connect4] Loading oracle keypair...`);
|
|
121
|
+
console.log(`🔴🟡 [Connect4] ORACLE_WALLET_JSON env exists: ${!!process.env.ORACLE_WALLET_JSON}`);
|
|
122
|
+
oracleKeypair = loadOracleKeypair();
|
|
123
|
+
if (oracleKeypair) {
|
|
124
|
+
console.log(`🔴🟡 [Connect4] Oracle wallet loaded: ${oracleKeypair.publicKey.toString()}`);
|
|
125
|
+
} else {
|
|
126
|
+
console.log(`🔴🟡 [Connect4] Failed to load oracle keypair!`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (!connection) {
|
|
130
|
+
connection = new Connection(RPC_URL, 'confirmed');
|
|
131
|
+
console.log(`🔴🟡 [Connect4] Solana connection initialized: ${RPC_URL}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Verify a transaction was successful on-chain
|
|
137
|
+
* Uses polling to check signature status (Alchemy-compatible, no WebSocket)
|
|
138
|
+
*
|
|
139
|
+
* @param {string} signature - Transaction signature to verify
|
|
140
|
+
* @param {number} timeout - Maximum time to wait in ms (default 15s)
|
|
141
|
+
* @returns {object} - { success: boolean, error?: string, status?: object }
|
|
142
|
+
*/
|
|
143
|
+
async function verifyTransactionSuccess(signature, timeout = 15000) {
|
|
144
|
+
// Ensure connection is initialized
|
|
145
|
+
if (!connection) {
|
|
146
|
+
initializeSolana();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!signature || typeof signature !== 'string' || signature.length < 80) {
|
|
150
|
+
console.log(`❌ [TxVerify] Invalid signature format: ${signature}`);
|
|
151
|
+
return { success: false, error: 'Invalid signature format' };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const start = Date.now();
|
|
155
|
+
console.log(`🔍 [TxVerify] Verifying transaction: ${signature}`);
|
|
156
|
+
console.log(`🔍 [TxVerify] Using RPC: ${connection.rpcEndpoint}`);
|
|
157
|
+
|
|
158
|
+
while (Date.now() - start < timeout) {
|
|
159
|
+
try {
|
|
160
|
+
const response = await connection.getSignatureStatuses([signature], {
|
|
161
|
+
searchTransactionHistory: true
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const status = response?.value?.[0];
|
|
165
|
+
console.log(`🔍 [TxVerify] Poll result:`, status ? JSON.stringify(status) : 'null');
|
|
166
|
+
|
|
167
|
+
if (status) {
|
|
168
|
+
// Check for error
|
|
169
|
+
if (status.err) {
|
|
170
|
+
console.log(`❌ [TxVerify] Transaction FAILED on-chain: ${JSON.stringify(status.err)}`);
|
|
171
|
+
return { success: false, error: `Transaction failed: ${JSON.stringify(status.err)}`, status };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Check confirmation level
|
|
175
|
+
if (status.confirmationStatus === 'confirmed' || status.confirmationStatus === 'finalized') {
|
|
176
|
+
console.log(`✅ [TxVerify] Transaction VERIFIED: ${status.confirmationStatus}`);
|
|
177
|
+
return { success: true, status };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Transaction found but not yet confirmed, keep polling
|
|
181
|
+
console.log(`⏳ [TxVerify] Transaction pending: ${status.confirmationStatus || 'processing'}`);
|
|
182
|
+
} else {
|
|
183
|
+
console.log(`⏳ [TxVerify] Transaction not found yet, polling...`);
|
|
184
|
+
}
|
|
185
|
+
} catch (pollErr) {
|
|
186
|
+
console.warn(`⚠️ [TxVerify] Poll error (will retry): ${pollErr.message}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Wait before next poll
|
|
190
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Timeout - transaction not found or not confirmed
|
|
194
|
+
console.log(`⚠️ [TxVerify] Transaction not confirmed within ${timeout}ms - REJECTING`);
|
|
195
|
+
return { success: false, error: `Transaction not confirmed within ${timeout}ms` };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Board dimensions
|
|
199
|
+
const ROWS = 6;
|
|
200
|
+
const COLS = 7;
|
|
201
|
+
const WINNING_LENGTH = 4;
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Create an empty 6x7 board
|
|
205
|
+
* Board is represented as a 2D array where:
|
|
206
|
+
* - null = empty cell
|
|
207
|
+
* - 'home' = player 1's piece
|
|
208
|
+
* - 'away' = player 2's piece
|
|
209
|
+
*
|
|
210
|
+
* Row 0 is the TOP of the board, Row 5 is the BOTTOM
|
|
211
|
+
*/
|
|
212
|
+
function createEmptyBoard() {
|
|
213
|
+
return Array(ROWS).fill(null).map(() => Array(COLS).fill(null));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Find the lowest empty row in a column (where a piece would land)
|
|
218
|
+
* @param {Array} board - 2D board array
|
|
219
|
+
* @param {number} col - Column index (0-6)
|
|
220
|
+
* @returns {number} Row index where piece lands, or -1 if column is full
|
|
221
|
+
*/
|
|
222
|
+
function getLowestEmptyRow(board, col) {
|
|
223
|
+
// Start from bottom (row 5) and work up
|
|
224
|
+
for (let row = ROWS - 1; row >= 0; row--) {
|
|
225
|
+
if (board[row][col] === null) {
|
|
226
|
+
return row;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return -1; // Column is full
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Check if a move results in a win
|
|
234
|
+
* @param {Array} board - 2D board array (after the move)
|
|
235
|
+
* @param {number} row - Row of the last placed piece
|
|
236
|
+
* @param {number} col - Column of the last placed piece
|
|
237
|
+
* @param {string} color - 'home' or 'away'
|
|
238
|
+
* @returns {Array|null} Array of winning cells [{row, col}] or null if no win
|
|
239
|
+
*/
|
|
240
|
+
function checkWin(board, row, col, color) {
|
|
241
|
+
// Check all 4 directions: horizontal, vertical, diagonal-down, diagonal-up
|
|
242
|
+
const directions = [
|
|
243
|
+
{ dr: 0, dc: 1 }, // horizontal
|
|
244
|
+
{ dr: 1, dc: 0 }, // vertical
|
|
245
|
+
{ dr: 1, dc: 1 }, // diagonal down-right
|
|
246
|
+
{ dr: 1, dc: -1 }, // diagonal down-left
|
|
247
|
+
];
|
|
248
|
+
|
|
249
|
+
for (const { dr, dc } of directions) {
|
|
250
|
+
const cells = getConnectedCells(board, row, col, dr, dc, color);
|
|
251
|
+
if (cells.length >= WINNING_LENGTH) {
|
|
252
|
+
// Return only the first 4 connected cells
|
|
253
|
+
return cells.slice(0, WINNING_LENGTH);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Get all connected cells in a direction (both ways)
|
|
262
|
+
*/
|
|
263
|
+
function getConnectedCells(board, row, col, dr, dc, color) {
|
|
264
|
+
const cells = [{ row, col }];
|
|
265
|
+
|
|
266
|
+
// Check positive direction
|
|
267
|
+
let r = row + dr;
|
|
268
|
+
let c = col + dc;
|
|
269
|
+
while (isValidCell(r, c) && board[r][c] === color) {
|
|
270
|
+
cells.push({ row: r, col: c });
|
|
271
|
+
r += dr;
|
|
272
|
+
c += dc;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Check negative direction
|
|
276
|
+
r = row - dr;
|
|
277
|
+
c = col - dc;
|
|
278
|
+
while (isValidCell(r, c) && board[r][c] === color) {
|
|
279
|
+
cells.unshift({ row: r, col: c });
|
|
280
|
+
r -= dr;
|
|
281
|
+
c -= dc;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return cells;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Check if cell coordinates are valid
|
|
289
|
+
*/
|
|
290
|
+
function isValidCell(row, col) {
|
|
291
|
+
return row >= 0 && row < ROWS && col >= 0 && col < COLS;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Check if the board is full (draw condition)
|
|
296
|
+
*/
|
|
297
|
+
function isBoardFull(board) {
|
|
298
|
+
// Check if top row has any empty cells
|
|
299
|
+
return board[0].every(cell => cell !== null);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Get game state from database and transform to Connect4Game format
|
|
304
|
+
* @param {string} gameId - Game ID
|
|
305
|
+
* @returns {Object} Connect4Game object
|
|
306
|
+
*/
|
|
307
|
+
async function getGameState(gameId) {
|
|
308
|
+
const result = await pool.query(`
|
|
309
|
+
SELECT
|
|
310
|
+
g.game_id,
|
|
311
|
+
g.game_address,
|
|
312
|
+
g.buy_in,
|
|
313
|
+
g.created_by,
|
|
314
|
+
g.game_status,
|
|
315
|
+
g.connect4_board,
|
|
316
|
+
g.connect4_current_turn,
|
|
317
|
+
g.connect4_winner,
|
|
318
|
+
g.connect4_winning_cells,
|
|
319
|
+
g.lock_timestamp,
|
|
320
|
+
g.created_at,
|
|
321
|
+
g.started_at,
|
|
322
|
+
g.completed_at,
|
|
323
|
+
g.invited_player,
|
|
324
|
+
g.image_url,
|
|
325
|
+
g.matchup_image_url,
|
|
326
|
+
-- Player 1 (creator - red)
|
|
327
|
+
u1.id as player1_user_id,
|
|
328
|
+
u1.username as player1_username,
|
|
329
|
+
u1.avatar as player1_avatar,
|
|
330
|
+
u1.wallet_address as player1_wallet,
|
|
331
|
+
-- Player 2 (joiner - yellow)
|
|
332
|
+
g.away_team_players[1] as player2_wallet,
|
|
333
|
+
u2.id as player2_user_id,
|
|
334
|
+
u2.username as player2_username,
|
|
335
|
+
u2.avatar as player2_avatar
|
|
336
|
+
FROM games g
|
|
337
|
+
LEFT JOIN users u1 ON g.created_by = u1.wallet_address
|
|
338
|
+
LEFT JOIN users u2 ON g.away_team_players[1] = u2.wallet_address
|
|
339
|
+
WHERE g.game_id = $1 AND g.game_type = 'connect4'
|
|
340
|
+
`, [gameId]);
|
|
341
|
+
|
|
342
|
+
if (result.rows.length === 0) {
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const row = result.rows[0];
|
|
347
|
+
|
|
348
|
+
// Get board - JSONB is already parsed by node-postgres, no need for JSON.parse
|
|
349
|
+
let board;
|
|
350
|
+
if (row.connect4_board) {
|
|
351
|
+
// If it's a string (shouldn't happen with JSONB but just in case), parse it
|
|
352
|
+
board = typeof row.connect4_board === 'string'
|
|
353
|
+
? JSON.parse(row.connect4_board)
|
|
354
|
+
: row.connect4_board;
|
|
355
|
+
} else {
|
|
356
|
+
board = createEmptyBoard();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Get winning cells - same as above, JSONB is already parsed
|
|
360
|
+
let winningCells = null;
|
|
361
|
+
if (row.connect4_winning_cells) {
|
|
362
|
+
winningCells = typeof row.connect4_winning_cells === 'string'
|
|
363
|
+
? JSON.parse(row.connect4_winning_cells)
|
|
364
|
+
: row.connect4_winning_cells;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Map database status to frontend status
|
|
368
|
+
let status = 'waiting';
|
|
369
|
+
if (row.game_status === 'playing' || row.game_status === 'in_progress') {
|
|
370
|
+
status = 'playing';
|
|
371
|
+
} else if (row.game_status === 'completed' || row.game_status === 'resolved') {
|
|
372
|
+
status = 'completed';
|
|
373
|
+
} else if (row.game_status === 'cancelled') {
|
|
374
|
+
status = 'cancelled';
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Build Connect4Game object
|
|
378
|
+
// Use homePlayer/awayPlayer naming to match frontend expectations
|
|
379
|
+
const game = {
|
|
380
|
+
gameId: row.game_id,
|
|
381
|
+
gameAddress: row.game_address,
|
|
382
|
+
buyIn: parseFloat(row.buy_in),
|
|
383
|
+
|
|
384
|
+
homePlayer: {
|
|
385
|
+
walletAddress: row.player1_wallet || row.created_by,
|
|
386
|
+
username: row.player1_username || formatWallet(row.created_by),
|
|
387
|
+
avatar: row.player1_avatar || null,
|
|
388
|
+
},
|
|
389
|
+
|
|
390
|
+
awayPlayer: row.player2_wallet ? {
|
|
391
|
+
walletAddress: row.player2_wallet,
|
|
392
|
+
username: row.player2_username || formatWallet(row.player2_wallet),
|
|
393
|
+
avatar: row.player2_avatar || null,
|
|
394
|
+
} : null,
|
|
395
|
+
|
|
396
|
+
invitedPlayer: row.invited_player || null, // For visibility filtering (private games)
|
|
397
|
+
imageUrl: row.image_url || null,
|
|
398
|
+
matchupImageUrl: row.matchup_image_url || null,
|
|
399
|
+
|
|
400
|
+
status,
|
|
401
|
+
board,
|
|
402
|
+
currentTurn: row.connect4_current_turn || 'home',
|
|
403
|
+
|
|
404
|
+
winner: row.connect4_winner || null,
|
|
405
|
+
winningCells,
|
|
406
|
+
winnerWallet: null, // Will be set below if there's a winner
|
|
407
|
+
winnerUsername: null,
|
|
408
|
+
winnerPrize: row.connect4_winner ? parseFloat(row.buy_in) * 2 * 0.99 : null, // 1% fee
|
|
409
|
+
|
|
410
|
+
createdAt: row.created_at,
|
|
411
|
+
startedAt: row.started_at,
|
|
412
|
+
completedAt: row.completed_at,
|
|
413
|
+
// Lock timestamp: stored value or computed as created_at + 130 seconds (2 min 10 sec)
|
|
414
|
+
lockTimestamp: row.lock_timestamp
|
|
415
|
+
? parseInt(row.lock_timestamp)
|
|
416
|
+
: (row.created_at ? Math.floor(new Date(row.created_at).getTime() / 1000) + 130 : null),
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
// Set winner info
|
|
420
|
+
if (row.connect4_winner === 'home' && game.homePlayer) {
|
|
421
|
+
game.winnerWallet = game.homePlayer.walletAddress;
|
|
422
|
+
game.winnerUsername = game.homePlayer.username;
|
|
423
|
+
} else if (row.connect4_winner === 'away' && game.awayPlayer) {
|
|
424
|
+
game.winnerWallet = game.awayPlayer.walletAddress;
|
|
425
|
+
game.winnerUsername = game.awayPlayer.username;
|
|
426
|
+
} else if (row.connect4_winner === 'draw') {
|
|
427
|
+
game.winnerWallet = 'draw';
|
|
428
|
+
game.winnerUsername = 'Draw';
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return game;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Process a move
|
|
436
|
+
* @param {string} gameId - Game ID
|
|
437
|
+
* @param {string} walletAddress - Wallet of player making the move
|
|
438
|
+
* @param {number} column - Column to drop piece (0-6)
|
|
439
|
+
* @returns {Object} Result with board, move details, win info
|
|
440
|
+
*/
|
|
441
|
+
async function processMove(gameId, walletAddress, column) {
|
|
442
|
+
// Get current game state
|
|
443
|
+
const game = await getGameState(gameId);
|
|
444
|
+
|
|
445
|
+
if (!game) {
|
|
446
|
+
throw new Error('Game not found');
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Allow moves when playing OR waiting (creator can make opening moves while waiting for opponent)
|
|
450
|
+
if (game.status !== 'playing' && game.status !== 'waiting') {
|
|
451
|
+
throw new Error(`Game is not in progress (status: ${game.status})`);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Determine player's color
|
|
455
|
+
let playerColor;
|
|
456
|
+
if (game.homePlayer?.walletAddress === walletAddress) {
|
|
457
|
+
playerColor = 'home';
|
|
458
|
+
} else if (game.awayPlayer?.walletAddress === walletAddress) {
|
|
459
|
+
playerColor = 'away';
|
|
460
|
+
} else {
|
|
461
|
+
throw new Error('You are not a player in this game');
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Check if it's their turn
|
|
465
|
+
if (game.currentTurn !== playerColor) {
|
|
466
|
+
throw new Error(`Not your turn. Current turn: ${game.currentTurn}`);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Validate column
|
|
470
|
+
if (column < 0 || column >= COLS) {
|
|
471
|
+
throw new Error(`Invalid column: ${column}. Must be 0-6`);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Find where piece lands
|
|
475
|
+
const row = getLowestEmptyRow(game.board, column);
|
|
476
|
+
if (row === -1) {
|
|
477
|
+
throw new Error('Column is full');
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Make the move
|
|
481
|
+
const board = game.board.map(r => [...r]); // Deep copy
|
|
482
|
+
board[row][column] = playerColor;
|
|
483
|
+
|
|
484
|
+
// Check for win
|
|
485
|
+
const winningCells = checkWin(board, row, column, playerColor);
|
|
486
|
+
const isWin = winningCells !== null;
|
|
487
|
+
|
|
488
|
+
// Check for draw
|
|
489
|
+
const isDraw = !isWin && isBoardFull(board);
|
|
490
|
+
|
|
491
|
+
// Determine next turn
|
|
492
|
+
const nextTurn = playerColor === 'home' ? 'away' : 'home';
|
|
493
|
+
|
|
494
|
+
// Determine game result
|
|
495
|
+
let winner = null;
|
|
496
|
+
let newStatus = 'playing';
|
|
497
|
+
|
|
498
|
+
if (isWin) {
|
|
499
|
+
winner = playerColor;
|
|
500
|
+
newStatus = 'completed';
|
|
501
|
+
} else if (isDraw) {
|
|
502
|
+
winner = 'draw';
|
|
503
|
+
newStatus = 'completed';
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Update database
|
|
507
|
+
await pool.query(`
|
|
508
|
+
UPDATE games SET
|
|
509
|
+
connect4_board = $1,
|
|
510
|
+
connect4_current_turn = $2,
|
|
511
|
+
connect4_winner = $3,
|
|
512
|
+
connect4_winning_cells = $4,
|
|
513
|
+
game_status = $5,
|
|
514
|
+
completed_at = $6,
|
|
515
|
+
updated_at = NOW()
|
|
516
|
+
WHERE game_id = $7
|
|
517
|
+
`, [
|
|
518
|
+
JSON.stringify(board),
|
|
519
|
+
isWin || isDraw ? null : nextTurn,
|
|
520
|
+
winner,
|
|
521
|
+
winningCells ? JSON.stringify(winningCells) : null,
|
|
522
|
+
newStatus,
|
|
523
|
+
(isWin || isDraw) ? new Date() : null,
|
|
524
|
+
gameId,
|
|
525
|
+
]);
|
|
526
|
+
|
|
527
|
+
// Get winner info if game ended
|
|
528
|
+
let winnerWallet = null;
|
|
529
|
+
let winnerUsername = null;
|
|
530
|
+
let winnerPrize = null;
|
|
531
|
+
|
|
532
|
+
if (isWin) {
|
|
533
|
+
if (playerColor === 'home') {
|
|
534
|
+
winnerWallet = game.homePlayer.walletAddress;
|
|
535
|
+
winnerUsername = game.homePlayer.username;
|
|
536
|
+
} else {
|
|
537
|
+
winnerWallet = game.awayPlayer.walletAddress;
|
|
538
|
+
winnerUsername = game.awayPlayer.username;
|
|
539
|
+
}
|
|
540
|
+
winnerPrize = game.buyIn * 2 * 0.95; // 5% total fee (1% oracle, 4% operator)
|
|
541
|
+
console.log(`🔴🟡 [Connect4] Game ended with win! Winner: ${winnerUsername} (${winnerWallet})`);
|
|
542
|
+
// NOTE: Payout is NOT automatic - winner must claim via /api/connect4/claim
|
|
543
|
+
} else if (isDraw) {
|
|
544
|
+
console.log(`🔴🟡 [Connect4] Game ended in draw! Players can claim refund via /api/connect4/claim`);
|
|
545
|
+
// NOTE: Refund is NOT automatic - players must claim via /api/connect4/claim
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return {
|
|
549
|
+
gameId,
|
|
550
|
+
column,
|
|
551
|
+
row,
|
|
552
|
+
color: playerColor,
|
|
553
|
+
board,
|
|
554
|
+
currentTurn: isWin || isDraw ? null : nextTurn,
|
|
555
|
+
winner,
|
|
556
|
+
winningCells,
|
|
557
|
+
winnerWallet,
|
|
558
|
+
winnerUsername,
|
|
559
|
+
winnerPrize,
|
|
560
|
+
isDraw,
|
|
561
|
+
isGameOver: isWin || isDraw,
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Format wallet address for display
|
|
567
|
+
*/
|
|
568
|
+
function formatWallet(wallet) {
|
|
569
|
+
if (!wallet) return 'Unknown';
|
|
570
|
+
return `${wallet.slice(0, 4)}...${wallet.slice(-4)}`;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Resolve a Connect4 game on-chain and distribute winnings
|
|
575
|
+
* @param {string} gameId - Game ID (c4-xxxxx format)
|
|
576
|
+
* @param {string} winner - 'home', 'away', or 'draw'
|
|
577
|
+
* @param {string} winnerWallet - Wallet address of the winner (null for draw)
|
|
578
|
+
* @returns {Object} Result with signature or error
|
|
579
|
+
*/
|
|
580
|
+
async function resolveGame(gameId, winner, winnerWallet) {
|
|
581
|
+
console.log(`🔴🟡 [Connect4] Resolving game ${gameId} - Winner: ${winner}`);
|
|
582
|
+
|
|
583
|
+
// Ensure Solana is initialized
|
|
584
|
+
initializeSolana();
|
|
585
|
+
|
|
586
|
+
if (!oracleKeypair) {
|
|
587
|
+
console.error('❌ [Connect4] Cannot resolve: No oracle keypair available');
|
|
588
|
+
return { success: false, error: 'Oracle keypair not available' };
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
try {
|
|
592
|
+
// Get game address and buy-in from database
|
|
593
|
+
const gameResult = await pool.query(
|
|
594
|
+
'SELECT game_address, created_by, buy_in FROM games WHERE game_id = $1',
|
|
595
|
+
[gameId]
|
|
596
|
+
);
|
|
597
|
+
|
|
598
|
+
if (gameResult.rows.length === 0) {
|
|
599
|
+
throw new Error('Game not found in database');
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const { game_address, created_by, buy_in } = gameResult.rows[0];
|
|
603
|
+
|
|
604
|
+
// Look up referrer for the game creator (for 1% commission)
|
|
605
|
+
let referrerWallet = null;
|
|
606
|
+
try {
|
|
607
|
+
// Find referrer by matching creator's referral_code to referrer's my_referral_code
|
|
608
|
+
const referrerResult = await pool.query(`
|
|
609
|
+
SELECT referrer.wallet_address as referrer_wallet
|
|
610
|
+
FROM users creator
|
|
611
|
+
JOIN users referrer ON creator.referral_code = referrer.my_referral_code
|
|
612
|
+
WHERE creator.wallet_address = $1
|
|
613
|
+
AND creator.referral_code IS NOT NULL`,
|
|
614
|
+
[created_by]
|
|
615
|
+
);
|
|
616
|
+
if (referrerResult.rows.length > 0 && referrerResult.rows[0].referrer_wallet) {
|
|
617
|
+
referrerWallet = referrerResult.rows[0].referrer_wallet;
|
|
618
|
+
console.log(`🔴🟡 [Connect4] Found referrer for creator: ${referrerWallet}`);
|
|
619
|
+
}
|
|
620
|
+
} catch (refErr) {
|
|
621
|
+
console.warn('⚠️ [Connect4] Could not look up referrer:', refErr.message);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Convert gameId to u64 for on-chain
|
|
625
|
+
const hash = crypto.createHash('sha256').update(gameId).digest();
|
|
626
|
+
const gameIdNum = hash.readBigUInt64LE(0);
|
|
627
|
+
const gameIdBuf = Buffer.alloc(8);
|
|
628
|
+
gameIdBuf.writeBigUInt64LE(gameIdNum);
|
|
629
|
+
|
|
630
|
+
// Get game PDA
|
|
631
|
+
const [gamePDA] = PublicKey.findProgramAddressSync(
|
|
632
|
+
[Buffer.from("game"), gameIdBuf],
|
|
633
|
+
PROGRAM_ID
|
|
634
|
+
);
|
|
635
|
+
|
|
636
|
+
console.log(`🔴🟡 [Connect4] Game PDA: ${gamePDA.toString()}`);
|
|
637
|
+
|
|
638
|
+
// Encode winning team
|
|
639
|
+
// Connect4: red = player1 = home, yellow = player2 = away
|
|
640
|
+
let winningTeamBytes;
|
|
641
|
+
if (winner === 'draw' || winner === null) {
|
|
642
|
+
winningTeamBytes = Buffer.from([0]); // None - refund all
|
|
643
|
+
console.log(`🔴🟡 [Connect4] Encoding winner: DRAW (refund)`);
|
|
644
|
+
} else if (winner === 'home') {
|
|
645
|
+
winningTeamBytes = Buffer.from([1, 0]); // Some(Home)
|
|
646
|
+
console.log(`🔴🟡 [Connect4] Encoding winner: HOME`);
|
|
647
|
+
} else if (winner === 'away') {
|
|
648
|
+
winningTeamBytes = Buffer.from([1, 1]); // Some(Away)
|
|
649
|
+
console.log(`🔴🟡 [Connect4] Encoding winner: AWAY`);
|
|
650
|
+
} else {
|
|
651
|
+
console.error(`⚠️ [Connect4] Unexpected winner value: ${winner}, defaulting to refund`);
|
|
652
|
+
winningTeamBytes = Buffer.from([0]); // None - refund all
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Build instruction data
|
|
656
|
+
const data = Buffer.concat([
|
|
657
|
+
RESOLVE_AUTO_DISCRIMINATOR,
|
|
658
|
+
gameIdBuf,
|
|
659
|
+
winningTeamBytes
|
|
660
|
+
]);
|
|
661
|
+
|
|
662
|
+
// Build accounts
|
|
663
|
+
const keys = [
|
|
664
|
+
{ pubkey: gamePDA, isSigner: false, isWritable: true },
|
|
665
|
+
{ pubkey: oracleKeypair.publicKey, isSigner: true, isWritable: true }, // Oracle receives 1% fee
|
|
666
|
+
{ pubkey: OPERATOR_WALLET, isSigner: false, isWritable: true }, // Operator receives 4% (or 5% if no referrer)
|
|
667
|
+
];
|
|
668
|
+
|
|
669
|
+
// Add referrer if exists (receives 1% commission)
|
|
670
|
+
if (referrerWallet) {
|
|
671
|
+
try {
|
|
672
|
+
const referrerPubkey = new PublicKey(referrerWallet);
|
|
673
|
+
keys.push({ pubkey: referrerPubkey, isSigner: false, isWritable: true });
|
|
674
|
+
console.log(`🔴🟡 [Connect4] Added referrer to transaction: ${referrerWallet}`);
|
|
675
|
+
} catch (e) {
|
|
676
|
+
console.warn(`⚠️ [Connect4] Invalid referrer wallet: ${referrerWallet}`);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Create instruction
|
|
681
|
+
const ix = new TransactionInstruction({
|
|
682
|
+
keys,
|
|
683
|
+
programId: PROGRAM_ID,
|
|
684
|
+
data,
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
// Build and send transaction
|
|
688
|
+
const tx = new Transaction().add(ix);
|
|
689
|
+
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash('confirmed');
|
|
690
|
+
tx.recentBlockhash = blockhash;
|
|
691
|
+
tx.feePayer = oracleKeypair.publicKey;
|
|
692
|
+
tx.sign(oracleKeypair);
|
|
693
|
+
|
|
694
|
+
const signature = await connection.sendRawTransaction(tx.serialize());
|
|
695
|
+
console.log(`🔴🟡 [Connect4] Resolve tx sent: ${signature}, waiting for confirmation...`);
|
|
696
|
+
|
|
697
|
+
// Wait for confirmation using polling (Alchemy-compatible, no WebSocket)
|
|
698
|
+
try {
|
|
699
|
+
await pollTransactionConfirmation(connection, signature, lastValidBlockHeight, 30000);
|
|
700
|
+
console.log(`✅ [Connect4] Game ${gameId} resolved on-chain! Signature: ${signature}`);
|
|
701
|
+
} catch (confirmErr) {
|
|
702
|
+
// Check if it's already resolved (common case)
|
|
703
|
+
if (confirmErr.message?.includes('AlreadyResolved')) {
|
|
704
|
+
console.log(`🔴🟡 [Connect4] Game already resolved, continuing...`);
|
|
705
|
+
} else {
|
|
706
|
+
console.error(`❌ [Connect4] Resolve confirmation failed:`, confirmErr.message);
|
|
707
|
+
throw confirmErr;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Update database to mark as resolved
|
|
712
|
+
await pool.query(`
|
|
713
|
+
UPDATE games SET
|
|
714
|
+
is_resolved = TRUE,
|
|
715
|
+
claim_signature = $1,
|
|
716
|
+
resolved_at = NOW(),
|
|
717
|
+
updated_at = NOW()
|
|
718
|
+
WHERE game_id = $2
|
|
719
|
+
`, [signature, gameId]);
|
|
720
|
+
|
|
721
|
+
console.log(`✅ [Connect4] Database updated: is_resolved=true, claim_signature=${signature}`);
|
|
722
|
+
|
|
723
|
+
// Update winner's user_game_refs with claim_signature for transaction history linking
|
|
724
|
+
if (winnerWallet) {
|
|
725
|
+
await pool.query(`
|
|
726
|
+
UPDATE user_game_refs SET
|
|
727
|
+
claim_signature = $1,
|
|
728
|
+
claimed_at = NOW(),
|
|
729
|
+
updated_at = NOW()
|
|
730
|
+
WHERE game_id = $2 AND wallet_address = $3
|
|
731
|
+
`, [signature, gameId, winnerWallet]);
|
|
732
|
+
console.log(`✅ [Connect4] Winner's user_game_refs updated with claim_signature`);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Record referral earning with payout signature (if referrer was paid)
|
|
736
|
+
if (referrerWallet) {
|
|
737
|
+
try {
|
|
738
|
+
// Calculate commission: 1% of total pot (buy_in * 2 for 2-player Connect4)
|
|
739
|
+
const potSizeLamports = Math.floor(parseFloat(buy_in) * 2 * 1e9);
|
|
740
|
+
const commissionLamports = Math.floor(potSizeLamports * 0.01);
|
|
741
|
+
|
|
742
|
+
// Record the on-chain commission (creates record if doesn't exist)
|
|
743
|
+
await referralEarningsService.recordOnChainCommission(
|
|
744
|
+
gameId,
|
|
745
|
+
referrerWallet,
|
|
746
|
+
commissionLamports,
|
|
747
|
+
true, // paidOnChain
|
|
748
|
+
signature, // txSignature
|
|
749
|
+
'connect4' // gameType
|
|
750
|
+
);
|
|
751
|
+
console.log(`✅ [Connect4] Referral earning recorded: ${commissionLamports / 1e9} SOL to ${referrerWallet.slice(0, 8)}...`);
|
|
752
|
+
} catch (refErr) {
|
|
753
|
+
console.warn('⚠️ [Connect4] Failed to record referral earning:', refErr.message);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
return { success: true, signature };
|
|
758
|
+
|
|
759
|
+
} catch (error) {
|
|
760
|
+
console.error(`❌ [Connect4] Failed to resolve game ${gameId}:`, error.message);
|
|
761
|
+
|
|
762
|
+
// Check if game was already resolved on-chain (error 6033)
|
|
763
|
+
if (error.message && error.message.includes('AlreadyResolved')) {
|
|
764
|
+
console.log(`🔴🟡 [Connect4] Game ${gameId} was already resolved on-chain, updating database`);
|
|
765
|
+
try {
|
|
766
|
+
await pool.query(`
|
|
767
|
+
UPDATE games SET
|
|
768
|
+
is_resolved = TRUE,
|
|
769
|
+
game_status = 'completed',
|
|
770
|
+
completed_at = COALESCE(completed_at, NOW()),
|
|
771
|
+
updated_at = NOW()
|
|
772
|
+
WHERE game_id = $1
|
|
773
|
+
`, [gameId]);
|
|
774
|
+
return { success: true, alreadyResolved: true, message: 'Game was already resolved on-chain' };
|
|
775
|
+
} catch (dbErr) {
|
|
776
|
+
console.error('❌ [Connect4] Failed to update database after AlreadyResolved:', dbErr.message);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// Even if on-chain fails, mark the game as completed in database
|
|
781
|
+
try {
|
|
782
|
+
await pool.query(`
|
|
783
|
+
UPDATE games SET
|
|
784
|
+
game_status = 'completed',
|
|
785
|
+
completed_at = NOW(),
|
|
786
|
+
updated_at = NOW()
|
|
787
|
+
WHERE game_id = $1
|
|
788
|
+
`, [gameId]);
|
|
789
|
+
} catch (dbErr) {
|
|
790
|
+
console.error('❌ [Connect4] Failed to update database:', dbErr.message);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
return { success: false, error: error.message };
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* Initialize database columns for Connect4 if not present
|
|
799
|
+
*/
|
|
800
|
+
async function initializeConnect4Columns() {
|
|
801
|
+
try {
|
|
802
|
+
// Add Connect4 specific columns if they don't exist
|
|
803
|
+
await pool.query(`
|
|
804
|
+
DO $$
|
|
805
|
+
BEGIN
|
|
806
|
+
-- Add connect4_board column
|
|
807
|
+
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
|
808
|
+
WHERE table_name = 'games' AND column_name = 'connect4_board') THEN
|
|
809
|
+
ALTER TABLE games ADD COLUMN connect4_board JSONB;
|
|
810
|
+
END IF;
|
|
811
|
+
|
|
812
|
+
-- Add connect4_current_turn column
|
|
813
|
+
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
|
814
|
+
WHERE table_name = 'games' AND column_name = 'connect4_current_turn') THEN
|
|
815
|
+
ALTER TABLE games ADD COLUMN connect4_current_turn VARCHAR(10) DEFAULT 'home';
|
|
816
|
+
END IF;
|
|
817
|
+
|
|
818
|
+
-- Add connect4_winner column
|
|
819
|
+
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
|
820
|
+
WHERE table_name = 'games' AND column_name = 'connect4_winner') THEN
|
|
821
|
+
ALTER TABLE games ADD COLUMN connect4_winner VARCHAR(10);
|
|
822
|
+
END IF;
|
|
823
|
+
|
|
824
|
+
-- Add connect4_winning_cells column
|
|
825
|
+
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
|
826
|
+
WHERE table_name = 'games' AND column_name = 'connect4_winning_cells') THEN
|
|
827
|
+
ALTER TABLE games ADD COLUMN connect4_winning_cells JSONB;
|
|
828
|
+
END IF;
|
|
829
|
+
|
|
830
|
+
-- Add started_at column if missing
|
|
831
|
+
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
|
832
|
+
WHERE table_name = 'games' AND column_name = 'started_at') THEN
|
|
833
|
+
ALTER TABLE games ADD COLUMN started_at TIMESTAMP;
|
|
834
|
+
END IF;
|
|
835
|
+
|
|
836
|
+
-- Add completed_at column if missing
|
|
837
|
+
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
|
838
|
+
WHERE table_name = 'games' AND column_name = 'completed_at') THEN
|
|
839
|
+
ALTER TABLE games ADD COLUMN completed_at TIMESTAMP;
|
|
840
|
+
END IF;
|
|
841
|
+
|
|
842
|
+
-- Add game_status column if missing
|
|
843
|
+
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
|
844
|
+
WHERE table_name = 'games' AND column_name = 'game_status') THEN
|
|
845
|
+
ALTER TABLE games ADD COLUMN game_status VARCHAR(20) DEFAULT 'waiting';
|
|
846
|
+
END IF;
|
|
847
|
+
END $$;
|
|
848
|
+
`);
|
|
849
|
+
console.log('🔴🟡 Connect4 database columns initialized');
|
|
850
|
+
} catch (error) {
|
|
851
|
+
console.error('Failed to initialize Connect4 columns:', error);
|
|
852
|
+
throw error;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* Start a game (called when player 2 joins)
|
|
858
|
+
* Sets status to 'playing' but preserves existing board and turn
|
|
859
|
+
* (creator may have made opening moves while waiting)
|
|
860
|
+
*/
|
|
861
|
+
async function startGame(gameId) {
|
|
862
|
+
console.log(`[startGame] 🔴🟡 Starting game ${gameId}...`);
|
|
863
|
+
|
|
864
|
+
// Get current game state to preserve any existing moves
|
|
865
|
+
const currentGame = await getGameState(gameId);
|
|
866
|
+
|
|
867
|
+
console.log(`[startGame] Current game state BEFORE update:`);
|
|
868
|
+
console.log(`[startGame] - status: ${currentGame?.status}`);
|
|
869
|
+
console.log(`[startGame] - currentTurn: ${currentGame?.currentTurn}`);
|
|
870
|
+
console.log(`[startGame] - board has moves: ${JSON.stringify(currentGame?.board) !== JSON.stringify(createEmptyBoard())}`);
|
|
871
|
+
|
|
872
|
+
// Check raw DB values
|
|
873
|
+
const rawResult = await pool.query(
|
|
874
|
+
'SELECT connect4_board, connect4_current_turn, game_status FROM games WHERE game_id = $1',
|
|
875
|
+
[gameId]
|
|
876
|
+
);
|
|
877
|
+
const rawRow = rawResult.rows[0];
|
|
878
|
+
console.log(`[startGame] Raw DB values:`);
|
|
879
|
+
console.log(`[startGame] - connect4_board: ${rawRow?.connect4_board ? 'HAS VALUE' : 'NULL'}`);
|
|
880
|
+
console.log(`[startGame] - connect4_current_turn: ${rawRow?.connect4_current_turn}`);
|
|
881
|
+
console.log(`[startGame] - game_status: ${rawRow?.game_status}`);
|
|
882
|
+
|
|
883
|
+
// Use existing board if moves were made, otherwise create empty
|
|
884
|
+
const board = currentGame?.board || createEmptyBoard();
|
|
885
|
+
// Preserve current turn (don't reset to red if moves were made)
|
|
886
|
+
const currentTurn = currentGame?.currentTurn || 'home';
|
|
887
|
+
|
|
888
|
+
console.log(`[startGame] Values to use in COALESCE fallback:`);
|
|
889
|
+
console.log(`[startGame] - board: ${JSON.stringify(board) === JSON.stringify(createEmptyBoard()) ? 'EMPTY' : 'HAS MOVES'}`);
|
|
890
|
+
console.log(`[startGame] - currentTurn: ${currentTurn}`);
|
|
891
|
+
|
|
892
|
+
await pool.query(`
|
|
893
|
+
UPDATE games SET
|
|
894
|
+
game_status = 'playing',
|
|
895
|
+
connect4_board = COALESCE(connect4_board, $1),
|
|
896
|
+
connect4_current_turn = COALESCE(connect4_current_turn, $2),
|
|
897
|
+
started_at = NOW(),
|
|
898
|
+
updated_at = NOW()
|
|
899
|
+
WHERE game_id = $3
|
|
900
|
+
`, [JSON.stringify(board), currentTurn, gameId]);
|
|
901
|
+
|
|
902
|
+
const finalGame = await getGameState(gameId);
|
|
903
|
+
|
|
904
|
+
console.log(`[startGame] Game state AFTER update:`);
|
|
905
|
+
console.log(`[startGame] - status: ${finalGame?.status}`);
|
|
906
|
+
console.log(`[startGame] - currentTurn: ${finalGame?.currentTurn}`);
|
|
907
|
+
console.log(`[startGame] - board has moves: ${JSON.stringify(finalGame?.board) !== JSON.stringify(createEmptyBoard())}`);
|
|
908
|
+
|
|
909
|
+
return finalGame;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* Claim prize for a completed Connect4 game
|
|
914
|
+
* Called by the winner (or any player for a draw)
|
|
915
|
+
* @param {string} gameId - Game ID
|
|
916
|
+
* @param {string} walletAddress - Wallet address of the claimer
|
|
917
|
+
* @returns {Object} Result with signature or error
|
|
918
|
+
*/
|
|
919
|
+
async function claimPrize(gameId, walletAddress) {
|
|
920
|
+
const logPrefix = `[Connect4 ClaimPrize ${gameId}]`;
|
|
921
|
+
console.log(`${logPrefix} ====== SERVICE STARTED ======`);
|
|
922
|
+
console.log(`${logPrefix} Claimer wallet: ${walletAddress}`);
|
|
923
|
+
|
|
924
|
+
// Get game state
|
|
925
|
+
console.log(`${logPrefix} Fetching game state...`);
|
|
926
|
+
const game = await getGameState(gameId);
|
|
927
|
+
|
|
928
|
+
if (!game) {
|
|
929
|
+
console.log(`${logPrefix} ❌ Game not found`);
|
|
930
|
+
return { success: false, error: 'Game not found' };
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
console.log(`${logPrefix} Game state:`, JSON.stringify({
|
|
934
|
+
status: game.status,
|
|
935
|
+
winner: game.winner,
|
|
936
|
+
homePlayer: game.homePlayer?.walletAddress,
|
|
937
|
+
awayPlayer: game.awayPlayer?.walletAddress,
|
|
938
|
+
buyIn: game.buyIn,
|
|
939
|
+
winnerWallet: game.winnerWallet,
|
|
940
|
+
}));
|
|
941
|
+
|
|
942
|
+
if (game.status !== 'completed' && game.status !== 'cancelled') {
|
|
943
|
+
console.log(`${logPrefix} ❌ Invalid game status: ${game.status}`);
|
|
944
|
+
return { success: false, error: 'Game is not completed yet' };
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// Check if already resolved on-chain
|
|
948
|
+
console.log(`${logPrefix} Checking if already resolved...`);
|
|
949
|
+
const claimCheck = await pool.query(
|
|
950
|
+
'SELECT is_resolved, claim_signature FROM games WHERE game_id = $1',
|
|
951
|
+
[gameId]
|
|
952
|
+
);
|
|
953
|
+
|
|
954
|
+
const isResolved = claimCheck.rows[0]?.is_resolved;
|
|
955
|
+
const existingSignature = claimCheck.rows[0]?.claim_signature;
|
|
956
|
+
console.log(`${logPrefix} Resolution status: is_resolved=${isResolved}, has_signature=${!!existingSignature}`);
|
|
957
|
+
|
|
958
|
+
// If game is already resolved on-chain, skip resolution and return success
|
|
959
|
+
// The endpoint will build a claim transaction for the user to sign
|
|
960
|
+
if (isResolved) {
|
|
961
|
+
console.log(`${logPrefix} ✅ Already resolved, returning existing signature`);
|
|
962
|
+
console.log(`${logPrefix} ====== SERVICE SUCCESS (already resolved) ======`);
|
|
963
|
+
return {
|
|
964
|
+
success: true,
|
|
965
|
+
alreadyResolved: true,
|
|
966
|
+
signature: existingSignature,
|
|
967
|
+
message: 'Game resolved, sign transaction to claim prize',
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// Verify the claimer is authorized
|
|
972
|
+
const isHomePlayer = game.homePlayer?.walletAddress === walletAddress;
|
|
973
|
+
const isAwayPlayer = game.awayPlayer?.walletAddress === walletAddress;
|
|
974
|
+
console.log(`${logPrefix} Authorization check: isHomePlayer=${isHomePlayer}, isAwayPlayer=${isAwayPlayer}`);
|
|
975
|
+
|
|
976
|
+
if (!isHomePlayer && !isAwayPlayer) {
|
|
977
|
+
console.log(`${logPrefix} ❌ Claimer not a player in this game`);
|
|
978
|
+
console.log(`${logPrefix} Claimer: ${walletAddress}`);
|
|
979
|
+
console.log(`${logPrefix} Home: ${game.homePlayer?.walletAddress}`);
|
|
980
|
+
console.log(`${logPrefix} Away: ${game.awayPlayer?.walletAddress}`);
|
|
981
|
+
return { success: false, error: 'You are not a player in this game' };
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// For wins, only the winner can claim
|
|
985
|
+
// For cancelled games (winner=null) or draws, anyone can claim their refund
|
|
986
|
+
console.log(`${logPrefix} Winner check: game.winner=${game.winner}`);
|
|
987
|
+
if (game.winner !== 'draw' && game.winner !== null) {
|
|
988
|
+
const winnerWalletCheck = game.winner === 'home'
|
|
989
|
+
? game.homePlayer?.walletAddress
|
|
990
|
+
: game.awayPlayer?.walletAddress;
|
|
991
|
+
|
|
992
|
+
console.log(`${logPrefix} Winner wallet: ${winnerWalletCheck}`);
|
|
993
|
+
if (walletAddress !== winnerWalletCheck) {
|
|
994
|
+
console.log(`${logPrefix} ❌ Claimer is not the winner`);
|
|
995
|
+
return { success: false, error: 'Only the winner can claim the prize' };
|
|
996
|
+
}
|
|
997
|
+
} else {
|
|
998
|
+
console.log(`${logPrefix} Game is draw/cancelled - any player can claim refund`);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// Attempt to resolve the game on-chain
|
|
1002
|
+
const winnerWallet = game.winner === 'draw' || game.winner === null
|
|
1003
|
+
? null
|
|
1004
|
+
: (game.winner === 'home' ? game.homePlayer?.walletAddress : game.awayPlayer?.walletAddress);
|
|
1005
|
+
|
|
1006
|
+
console.log(`${logPrefix} Calling resolveGame with winner=${game.winner}, winnerWallet=${winnerWallet}...`);
|
|
1007
|
+
const resolveStartTime = Date.now();
|
|
1008
|
+
const result = await resolveGame(gameId, game.winner, winnerWallet);
|
|
1009
|
+
console.log(`${logPrefix} resolveGame completed in ${Date.now() - resolveStartTime}ms`);
|
|
1010
|
+
console.log(`${logPrefix} resolveGame result:`, JSON.stringify({
|
|
1011
|
+
success: result.success,
|
|
1012
|
+
alreadyResolved: result.alreadyResolved,
|
|
1013
|
+
signature: result.signature,
|
|
1014
|
+
error: result.error,
|
|
1015
|
+
}));
|
|
1016
|
+
|
|
1017
|
+
if (result.success) {
|
|
1018
|
+
console.log(`${logPrefix} ====== SERVICE SUCCESS ======`);
|
|
1019
|
+
return {
|
|
1020
|
+
success: true,
|
|
1021
|
+
alreadyResolved: result.alreadyResolved || false,
|
|
1022
|
+
signature: result.signature,
|
|
1023
|
+
message: result.alreadyResolved
|
|
1024
|
+
? 'Game already resolved - prize ready to claim!'
|
|
1025
|
+
: (game.winner === 'draw' || game.winner === null ? 'Refund processed!' : 'Prize claimed successfully!'),
|
|
1026
|
+
};
|
|
1027
|
+
} else {
|
|
1028
|
+
console.log(`${logPrefix} ❌ resolveGame failed: ${result.error}`);
|
|
1029
|
+
console.log(`${logPrefix} ====== SERVICE FAILED ======`);
|
|
1030
|
+
return { success: false, error: result.error || 'Failed to claim prize' };
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
module.exports = {
|
|
1035
|
+
createEmptyBoard,
|
|
1036
|
+
getLowestEmptyRow,
|
|
1037
|
+
checkWin,
|
|
1038
|
+
isBoardFull,
|
|
1039
|
+
getGameState,
|
|
1040
|
+
processMove,
|
|
1041
|
+
resolveGame,
|
|
1042
|
+
claimPrize,
|
|
1043
|
+
initializeConnect4Columns,
|
|
1044
|
+
initializeSolana,
|
|
1045
|
+
startGame,
|
|
1046
|
+
verifyTransactionSuccess,
|
|
1047
|
+
ROWS,
|
|
1048
|
+
COLS,
|
|
1049
|
+
};
|