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,699 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Solana Actions Routes - Blink integration for X/Twitter sharing
|
|
3
|
+
*
|
|
4
|
+
* ISOLATED MODULE - Can be safely deleted if not needed
|
|
5
|
+
*
|
|
6
|
+
* This module implements Solana Actions specification to allow users
|
|
7
|
+
* to join bets directly from X/Twitter without visiting the site.
|
|
8
|
+
*
|
|
9
|
+
* Endpoints:
|
|
10
|
+
* GET /api/actions/join-bet/:gameId - Returns action metadata (icon, buttons)
|
|
11
|
+
* POST /api/actions/join-bet/:gameId - Returns signable join transaction
|
|
12
|
+
*
|
|
13
|
+
* Feature flag: ENABLE_SOLANA_ACTIONS=true in .env to enable
|
|
14
|
+
*
|
|
15
|
+
* @see https://solana.com/docs/advanced/actions
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const express = require('express');
|
|
19
|
+
const router = express.Router();
|
|
20
|
+
const { Connection, PublicKey, SystemProgram, Transaction, TransactionInstruction } = require('@solana/web3.js');
|
|
21
|
+
const crypto = require('crypto');
|
|
22
|
+
|
|
23
|
+
// ============ CONFIGURATION ============
|
|
24
|
+
|
|
25
|
+
const PROGRAM_ID = new PublicKey(process.env.PROGRAM_ID || "85wJGp9uc8w2FeKX9CEHsudTo1UVCrmuRFy37oCcaoG1");
|
|
26
|
+
const LAMPORTS_PER_SOL = 1_000_000_000;
|
|
27
|
+
|
|
28
|
+
// Join automatic game discriminator (must match server.js)
|
|
29
|
+
const JOIN_AUTO = Buffer.from([87, 51, 29, 81, 147, 216, 222, 119]);
|
|
30
|
+
|
|
31
|
+
// Default bet amounts for action buttons (in SOL)
|
|
32
|
+
const DEFAULT_BET_AMOUNTS = [0.05, 0.1, 0.25];
|
|
33
|
+
|
|
34
|
+
// ============ SOCKET.IO INJECTION ============
|
|
35
|
+
|
|
36
|
+
let chatNamespace = null;
|
|
37
|
+
|
|
38
|
+
// Inject Socket.IO instance (called from server.js)
|
|
39
|
+
router.setSocketIO = (ioInstance, chatNS) => {
|
|
40
|
+
chatNamespace = chatNS;
|
|
41
|
+
console.log('🔌 Socket.IO injected into actions routes');
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// ============ HELPER FUNCTIONS (copied to keep isolated) ============
|
|
45
|
+
|
|
46
|
+
function uuidToU64(uuid) {
|
|
47
|
+
const hash = crypto.createHash('sha256').update(uuid).digest();
|
|
48
|
+
return hash.readBigUInt64LE(0);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getGamePDA(gameId) {
|
|
52
|
+
let gameIdNum;
|
|
53
|
+
if (typeof gameId === 'string' && gameId.includes('-')) {
|
|
54
|
+
gameIdNum = uuidToU64(gameId);
|
|
55
|
+
} else {
|
|
56
|
+
gameIdNum = BigInt(gameId);
|
|
57
|
+
}
|
|
58
|
+
const gameIdBuf = Buffer.alloc(8);
|
|
59
|
+
gameIdBuf.writeBigUInt64LE(gameIdNum);
|
|
60
|
+
return PublicKey.findProgramAddressSync([Buffer.from("game"), gameIdBuf], PROGRAM_ID);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getGameIdBuffer(gameId) {
|
|
64
|
+
let gameIdNum;
|
|
65
|
+
if (typeof gameId === 'string' && gameId.includes('-')) {
|
|
66
|
+
gameIdNum = uuidToU64(gameId);
|
|
67
|
+
} else {
|
|
68
|
+
gameIdNum = BigInt(gameId);
|
|
69
|
+
}
|
|
70
|
+
const buf = Buffer.alloc(8);
|
|
71
|
+
buf.writeBigUInt64LE(gameIdNum);
|
|
72
|
+
return buf;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ============ CORS MIDDLEWARE FOR ACTIONS ============
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Solana Actions require specific CORS headers
|
|
79
|
+
* This middleware adds them to all routes in this file
|
|
80
|
+
*/
|
|
81
|
+
router.use((req, res, next) => {
|
|
82
|
+
// CORS headers required by Solana Actions spec
|
|
83
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
84
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, OPTIONS');
|
|
85
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Content-Encoding, Accept-Encoding, Accept, X-Requested-With');
|
|
86
|
+
res.setHeader('Access-Control-Expose-Headers', 'X-Action-Version, X-Blockchain-Ids');
|
|
87
|
+
res.setHeader('X-Action-Version', '2.2');
|
|
88
|
+
// Set blockchain ID based on environment (devnet vs mainnet)
|
|
89
|
+
const isDevnet = (process.env.SOLANA_NETWORK || '').includes('devnet');
|
|
90
|
+
const blockchainId = isDevnet
|
|
91
|
+
? 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1' // devnet
|
|
92
|
+
: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'; // mainnet
|
|
93
|
+
res.setHeader('X-Blockchain-Ids', blockchainId);
|
|
94
|
+
|
|
95
|
+
if (req.method === 'OPTIONS') {
|
|
96
|
+
return res.status(200).end();
|
|
97
|
+
}
|
|
98
|
+
next();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// ============ FACTORY FUNCTION ============
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Create actions routes with database pool injection
|
|
105
|
+
* @param {Pool} pool - PostgreSQL connection pool
|
|
106
|
+
* @param {Connection} connection - Solana RPC connection
|
|
107
|
+
*/
|
|
108
|
+
function createActionsRoutes(pool, connection) {
|
|
109
|
+
|
|
110
|
+
// ============ AUTO-CREATE USER FOR BLINK JOINS ============
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Find or create a minimal user record for Blink joins
|
|
114
|
+
* Users created this way are marked as needing onboarding
|
|
115
|
+
*/
|
|
116
|
+
async function findOrCreateBlinkUser(walletAddress) {
|
|
117
|
+
try {
|
|
118
|
+
// Check if user exists
|
|
119
|
+
const existingUser = await pool.query(
|
|
120
|
+
'SELECT id, username FROM users WHERE wallet_address = $1',
|
|
121
|
+
[walletAddress]
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
if (existingUser.rows.length > 0) {
|
|
125
|
+
return { exists: true, user: existingUser.rows[0] };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Create minimal user for Blink join
|
|
129
|
+
// Uses existing columns only - no schema changes needed
|
|
130
|
+
const shortWallet = walletAddress.slice(0, 6);
|
|
131
|
+
const autoUsername = `blink_${shortWallet}`;
|
|
132
|
+
|
|
133
|
+
const result = await pool.query(
|
|
134
|
+
`INSERT INTO users
|
|
135
|
+
(wallet_address, username, created_at, onboarding_complete)
|
|
136
|
+
VALUES ($1, $2, NOW(), false)
|
|
137
|
+
ON CONFLICT (wallet_address) DO NOTHING
|
|
138
|
+
RETURNING id, username`,
|
|
139
|
+
[walletAddress, autoUsername]
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
if (result.rows.length > 0) {
|
|
143
|
+
console.log(`[Actions] Created Blink user: ${autoUsername} (${walletAddress.slice(0, 8)}...)`);
|
|
144
|
+
return { exists: false, created: true, user: result.rows[0] };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Race condition: user was created between check and insert
|
|
148
|
+
const raceUser = await pool.query(
|
|
149
|
+
'SELECT id, username FROM users WHERE wallet_address = $1',
|
|
150
|
+
[walletAddress]
|
|
151
|
+
);
|
|
152
|
+
return { exists: true, user: raceUser.rows[0] };
|
|
153
|
+
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error('[Actions] Error in findOrCreateBlinkUser:', error.message);
|
|
156
|
+
// Don't fail the transaction - just log and continue
|
|
157
|
+
return { exists: false, created: false, error: error.message };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ============ GET ENDPOINT - ACTION METADATA ============
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* GET /api/actions/join-bet/:gameId
|
|
165
|
+
* Returns action metadata for rendering the Blink UI
|
|
166
|
+
*/
|
|
167
|
+
router.get('/join-bet/:gameId', async (req, res) => {
|
|
168
|
+
try {
|
|
169
|
+
const { gameId } = req.params;
|
|
170
|
+
|
|
171
|
+
// Fetch game from database
|
|
172
|
+
const gameResult = await pool.query(
|
|
173
|
+
`SELECT
|
|
174
|
+
game_id, title, buy_in, is_locked, is_resolved,
|
|
175
|
+
matchup_image_url, sports_event, lock_timestamp,
|
|
176
|
+
home_team_players, away_team_players, draw_team_players,
|
|
177
|
+
total_pool, home_pool, away_pool, draw_pool
|
|
178
|
+
FROM games WHERE game_id = $1`,
|
|
179
|
+
[gameId]
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
if (gameResult.rows.length === 0) {
|
|
183
|
+
return res.status(404).json({
|
|
184
|
+
icon: 'https://dubs.app/logo.png',
|
|
185
|
+
title: 'Game Not Found',
|
|
186
|
+
description: 'This bet no longer exists',
|
|
187
|
+
label: 'Not Found',
|
|
188
|
+
disabled: true,
|
|
189
|
+
error: { message: 'Game not found' }
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const game = gameResult.rows[0];
|
|
194
|
+
const sportsEvent = game.sports_event || {};
|
|
195
|
+
|
|
196
|
+
// Check if game is joinable
|
|
197
|
+
if (game.is_locked) {
|
|
198
|
+
return res.json({
|
|
199
|
+
icon: game.matchup_image_url || 'https://dubs.app/logo.png',
|
|
200
|
+
title: game.title || 'Betting Closed',
|
|
201
|
+
description: 'Betting has closed for this game',
|
|
202
|
+
label: 'Closed',
|
|
203
|
+
disabled: true,
|
|
204
|
+
error: { message: 'Betting is closed' }
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (game.is_resolved) {
|
|
209
|
+
return res.json({
|
|
210
|
+
icon: game.matchup_image_url || 'https://dubs.app/logo.png',
|
|
211
|
+
title: game.title || 'Game Finished',
|
|
212
|
+
description: 'This game has already been resolved',
|
|
213
|
+
label: 'Finished',
|
|
214
|
+
disabled: true,
|
|
215
|
+
error: { message: 'Game is finished' }
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Extract team names
|
|
220
|
+
const homeTeam = sportsEvent.strHomeTeam || 'Home';
|
|
221
|
+
const awayTeam = sportsEvent.strAwayTeam || 'Away';
|
|
222
|
+
const hasDrawOption = sportsEvent.strSport === 'Soccer' ||
|
|
223
|
+
sportsEvent.strLeague?.includes('MLS') ||
|
|
224
|
+
sportsEvent.strLeague?.includes('Premier League');
|
|
225
|
+
|
|
226
|
+
// Build base URL from request (required for absolute hrefs)
|
|
227
|
+
const protocol = req.headers['x-forwarded-proto'] || req.protocol || 'https';
|
|
228
|
+
const host = req.headers['x-forwarded-host'] || req.headers.host;
|
|
229
|
+
const baseUrl = `${protocol}://${host}`;
|
|
230
|
+
|
|
231
|
+
// Build action links for each team
|
|
232
|
+
const actions = [];
|
|
233
|
+
|
|
234
|
+
// Home team action with amount parameter
|
|
235
|
+
actions.push({
|
|
236
|
+
type: 'transaction',
|
|
237
|
+
label: `${homeTeam}`,
|
|
238
|
+
href: `${baseUrl}/api/actions/join-bet/${gameId}?team=home&amount={amount}`,
|
|
239
|
+
parameters: [
|
|
240
|
+
{
|
|
241
|
+
name: 'amount',
|
|
242
|
+
label: 'Bet Amount (SOL)',
|
|
243
|
+
required: true,
|
|
244
|
+
type: 'number',
|
|
245
|
+
min: 0.01,
|
|
246
|
+
max: 10,
|
|
247
|
+
}
|
|
248
|
+
]
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// Away team action with amount parameter
|
|
252
|
+
actions.push({
|
|
253
|
+
type: 'transaction',
|
|
254
|
+
label: `${awayTeam}`,
|
|
255
|
+
href: `${baseUrl}/api/actions/join-bet/${gameId}?team=away&amount={amount}`,
|
|
256
|
+
parameters: [
|
|
257
|
+
{
|
|
258
|
+
name: 'amount',
|
|
259
|
+
label: 'Bet Amount (SOL)',
|
|
260
|
+
required: true,
|
|
261
|
+
type: 'number',
|
|
262
|
+
min: 0.01,
|
|
263
|
+
max: 10,
|
|
264
|
+
}
|
|
265
|
+
]
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// Draw option for soccer
|
|
269
|
+
if (hasDrawOption) {
|
|
270
|
+
actions.push({
|
|
271
|
+
type: 'transaction',
|
|
272
|
+
label: 'Draw',
|
|
273
|
+
href: `${baseUrl}/api/actions/join-bet/${gameId}?team=draw&amount={amount}`,
|
|
274
|
+
parameters: [
|
|
275
|
+
{
|
|
276
|
+
name: 'amount',
|
|
277
|
+
label: 'Bet Amount (SOL)',
|
|
278
|
+
required: true,
|
|
279
|
+
type: 'number',
|
|
280
|
+
min: 0.01,
|
|
281
|
+
max: 10,
|
|
282
|
+
}
|
|
283
|
+
]
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Format pool info for description
|
|
288
|
+
const totalPool = parseFloat(game.total_pool) || 0;
|
|
289
|
+
const playerCount = (game.home_team_players?.length || 0) +
|
|
290
|
+
(game.away_team_players?.length || 0) +
|
|
291
|
+
(game.draw_team_players?.length || 0);
|
|
292
|
+
|
|
293
|
+
const description = totalPool > 0
|
|
294
|
+
? `${playerCount} player${playerCount !== 1 ? 's' : ''} | ${totalPool.toFixed(2)} SOL pot`
|
|
295
|
+
: 'Be the first to bet!';
|
|
296
|
+
|
|
297
|
+
// Return action metadata
|
|
298
|
+
res.json({
|
|
299
|
+
type: 'action',
|
|
300
|
+
icon: game.matchup_image_url || 'https://dubs.app/logo.png',
|
|
301
|
+
title: game.title || `${homeTeam} vs ${awayTeam}`,
|
|
302
|
+
description: description,
|
|
303
|
+
label: 'Place Bet',
|
|
304
|
+
links: {
|
|
305
|
+
actions: actions
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
} catch (error) {
|
|
310
|
+
console.error('[Actions] GET error:', error);
|
|
311
|
+
res.status(500).json({
|
|
312
|
+
icon: 'https://dubs.app/logo.png',
|
|
313
|
+
title: 'Error',
|
|
314
|
+
description: 'Failed to load game',
|
|
315
|
+
label: 'Error',
|
|
316
|
+
disabled: true,
|
|
317
|
+
error: { message: error.message }
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// ============ POST ENDPOINT - BUILD TRANSACTION ============
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* POST /api/actions/join-bet/:gameId
|
|
326
|
+
* Returns a signable transaction to join the bet
|
|
327
|
+
*/
|
|
328
|
+
router.post('/join-bet/:gameId', async (req, res) => {
|
|
329
|
+
try {
|
|
330
|
+
const { gameId } = req.params;
|
|
331
|
+
const { team, amount } = req.query;
|
|
332
|
+
const { account } = req.body; // User's wallet address (base58)
|
|
333
|
+
|
|
334
|
+
// Build base URL for callback
|
|
335
|
+
const protocol = req.headers['x-forwarded-proto'] || req.protocol || 'https';
|
|
336
|
+
const host = req.headers['x-forwarded-host'] || req.headers.host;
|
|
337
|
+
const baseUrl = `${protocol}://${host}`;
|
|
338
|
+
|
|
339
|
+
// Validate inputs
|
|
340
|
+
if (!account) {
|
|
341
|
+
return res.status(400).json({
|
|
342
|
+
error: { message: 'Wallet account is required' }
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (!team || !['home', 'away', 'draw'].includes(team)) {
|
|
347
|
+
return res.status(400).json({
|
|
348
|
+
error: { message: 'Team must be home, away, or draw' }
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const betAmount = parseFloat(amount);
|
|
353
|
+
if (!betAmount || betAmount <= 0 || betAmount > 10) {
|
|
354
|
+
return res.status(400).json({
|
|
355
|
+
error: { message: 'Amount must be between 0.01 and 10 SOL' }
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Validate wallet address format
|
|
360
|
+
let playerPubkey;
|
|
361
|
+
try {
|
|
362
|
+
playerPubkey = new PublicKey(account);
|
|
363
|
+
} catch (e) {
|
|
364
|
+
return res.status(400).json({
|
|
365
|
+
error: { message: 'Invalid wallet address' }
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Verify game exists and is joinable
|
|
370
|
+
const gameResult = await pool.query(
|
|
371
|
+
`SELECT game_id, is_locked, is_resolved, title, sports_event,
|
|
372
|
+
home_team_players, away_team_players, draw_team_players
|
|
373
|
+
FROM games WHERE game_id = $1`,
|
|
374
|
+
[gameId]
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
if (gameResult.rows.length === 0) {
|
|
378
|
+
return res.status(404).json({
|
|
379
|
+
type: 'action',
|
|
380
|
+
icon: 'https://dubs.app/logo.png',
|
|
381
|
+
title: 'Game Not Found',
|
|
382
|
+
description: 'This bet no longer exists',
|
|
383
|
+
label: 'Not Found',
|
|
384
|
+
disabled: true,
|
|
385
|
+
error: { message: 'Game not found' }
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const game = gameResult.rows[0];
|
|
390
|
+
|
|
391
|
+
if (game.is_locked) {
|
|
392
|
+
return res.status(400).json({
|
|
393
|
+
type: 'action',
|
|
394
|
+
icon: game.matchup_image_url || 'https://dubs.app/logo.png',
|
|
395
|
+
title: 'Betting Closed',
|
|
396
|
+
description: 'Betting has closed for this game',
|
|
397
|
+
label: 'Closed',
|
|
398
|
+
disabled: true,
|
|
399
|
+
error: { message: 'Betting has closed for this game' }
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (game.is_resolved) {
|
|
404
|
+
return res.status(400).json({
|
|
405
|
+
type: 'action',
|
|
406
|
+
icon: game.matchup_image_url || 'https://dubs.app/logo.png',
|
|
407
|
+
title: 'Game Finished',
|
|
408
|
+
description: 'This game has already been resolved',
|
|
409
|
+
label: 'Finished',
|
|
410
|
+
disabled: true,
|
|
411
|
+
error: { message: 'This game has already been resolved' }
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Check if player already joined
|
|
416
|
+
const allPlayers = [
|
|
417
|
+
...(game.home_team_players || []),
|
|
418
|
+
...(game.away_team_players || []),
|
|
419
|
+
...(game.draw_team_players || [])
|
|
420
|
+
];
|
|
421
|
+
|
|
422
|
+
if (allPlayers.includes(account)) {
|
|
423
|
+
return res.status(400).json({
|
|
424
|
+
type: 'action',
|
|
425
|
+
icon: game.matchup_image_url || 'https://dubs.app/logo.png',
|
|
426
|
+
title: 'Already Joined',
|
|
427
|
+
description: 'You have already joined this bet',
|
|
428
|
+
label: 'Already Joined',
|
|
429
|
+
disabled: true,
|
|
430
|
+
error: { message: 'You have already joined this bet' }
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Auto-create user if needed (for tracking/notifications)
|
|
435
|
+
await findOrCreateBlinkUser(account);
|
|
436
|
+
|
|
437
|
+
// Build the join transaction
|
|
438
|
+
const [gamePDA] = getGamePDA(gameId);
|
|
439
|
+
const gameIdBuf = getGameIdBuffer(gameId);
|
|
440
|
+
|
|
441
|
+
// Convert amount to lamports
|
|
442
|
+
const amountBuf = Buffer.alloc(8);
|
|
443
|
+
amountBuf.writeBigUInt64LE(BigInt(Math.round(betAmount * LAMPORTS_PER_SOL)));
|
|
444
|
+
|
|
445
|
+
// Encode team choice: 0 = Home, 1 = Away, 2 = Draw
|
|
446
|
+
const teamChoiceByte = team === 'home' ? 0 : team === 'away' ? 1 : 2;
|
|
447
|
+
|
|
448
|
+
// Build instruction data for join_automatic_game
|
|
449
|
+
const data = Buffer.concat([
|
|
450
|
+
JOIN_AUTO,
|
|
451
|
+
gameIdBuf,
|
|
452
|
+
Buffer.from([teamChoiceByte]),
|
|
453
|
+
amountBuf
|
|
454
|
+
]);
|
|
455
|
+
|
|
456
|
+
const instruction = new TransactionInstruction({
|
|
457
|
+
keys: [
|
|
458
|
+
{ pubkey: gamePDA, isSigner: false, isWritable: true },
|
|
459
|
+
{ pubkey: playerPubkey, isSigner: true, isWritable: true },
|
|
460
|
+
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
|
|
461
|
+
],
|
|
462
|
+
programId: PROGRAM_ID,
|
|
463
|
+
data,
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
const transaction = new Transaction().add(instruction);
|
|
467
|
+
transaction.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
|
|
468
|
+
transaction.feePayer = playerPubkey;
|
|
469
|
+
|
|
470
|
+
const serialized = transaction.serialize({ requireAllSignatures: false }).toString('base64');
|
|
471
|
+
|
|
472
|
+
// Get team name for message
|
|
473
|
+
const sportsEvent = game.sports_event || {};
|
|
474
|
+
const teamName = team === 'home' ? (sportsEvent.strHomeTeam || 'Home') :
|
|
475
|
+
team === 'away' ? (sportsEvent.strAwayTeam || 'Away') : 'Draw';
|
|
476
|
+
|
|
477
|
+
console.log(`[Actions] Built join tx for ${account.slice(0, 8)}... | ${betAmount} SOL on ${teamName} | Game: ${gameId.slice(-8)}`);
|
|
478
|
+
|
|
479
|
+
res.json({
|
|
480
|
+
type: 'transaction',
|
|
481
|
+
transaction: serialized,
|
|
482
|
+
message: `Bet ${betAmount} SOL on ${teamName}`,
|
|
483
|
+
links: {
|
|
484
|
+
next: {
|
|
485
|
+
type: 'post',
|
|
486
|
+
href: `${baseUrl}/api/actions/confirm-join/${gameId}?team=${team}&amount=${betAmount}`
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
} catch (error) {
|
|
492
|
+
console.error('[Actions] POST error:', error);
|
|
493
|
+
res.status(500).json({
|
|
494
|
+
error: { message: error.message || 'Failed to build transaction' }
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
// ============ CONFIRM ENDPOINT - POST-TRANSACTION DATABASE UPDATE ============
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* POST /api/actions/confirm-join/:gameId
|
|
503
|
+
* Called by Blink client after transaction confirms - updates the database
|
|
504
|
+
*/
|
|
505
|
+
router.post('/confirm-join/:gameId', async (req, res) => {
|
|
506
|
+
try {
|
|
507
|
+
const { gameId } = req.params;
|
|
508
|
+
const { team, amount } = req.query;
|
|
509
|
+
const { account, signature } = req.body; // account + signature from Blink client
|
|
510
|
+
|
|
511
|
+
console.log(`[Actions] Confirm join: ${account?.slice(0, 8)}... | Game: ${gameId.slice(-8)} | Sig: ${signature?.slice(0, 16)}...`);
|
|
512
|
+
|
|
513
|
+
if (!account || !signature) {
|
|
514
|
+
return res.status(400).json({
|
|
515
|
+
error: { message: 'Account and signature are required' }
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const wallet = account;
|
|
520
|
+
|
|
521
|
+
const betAmount = parseFloat(amount) || 0;
|
|
522
|
+
|
|
523
|
+
// Verify team choice from on-chain transaction (source of truth)
|
|
524
|
+
// The query param `team` can diverge from what was actually signed on-chain
|
|
525
|
+
let verifiedTeam = team;
|
|
526
|
+
try {
|
|
527
|
+
const txInfo = await connection.getTransaction(signature, {
|
|
528
|
+
commitment: 'confirmed',
|
|
529
|
+
maxSupportedTransactionVersion: 0
|
|
530
|
+
});
|
|
531
|
+
if (txInfo?.transaction?.message) {
|
|
532
|
+
const msg = txInfo.transaction.message;
|
|
533
|
+
const programIdStr = PROGRAM_ID.toString();
|
|
534
|
+
const accountKeys = msg.staticAccountKeys || msg.accountKeys || [];
|
|
535
|
+
const instructions = msg.compiledInstructions || msg.instructions || [];
|
|
536
|
+
for (const ix of instructions) {
|
|
537
|
+
const progIdx = ix.programIdIndex;
|
|
538
|
+
const progKey = accountKeys[progIdx]?.toString();
|
|
539
|
+
if (progKey === programIdStr) {
|
|
540
|
+
const data = Buffer.from(ix.data);
|
|
541
|
+
// join_automatic_game: 8-byte discriminator + 8-byte gameId + 1-byte teamChoice
|
|
542
|
+
if (data.length >= 17 && data.slice(0, 8).equals(JOIN_AUTO)) {
|
|
543
|
+
const teamByte = data[16];
|
|
544
|
+
const onChainTeam = teamByte === 0 ? 'home' : teamByte === 1 ? 'away' : 'draw';
|
|
545
|
+
if (onChainTeam !== team) {
|
|
546
|
+
console.warn(`[Actions] ⚠️ TEAM MISMATCH: query=${team}, on-chain=${onChainTeam} | Wallet: ${wallet.slice(0, 8)}... | Game: ${gameId}`);
|
|
547
|
+
}
|
|
548
|
+
verifiedTeam = onChainTeam;
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
} catch (err) {
|
|
555
|
+
console.warn(`[Actions] Could not verify on-chain team choice, using query param: ${err.message}`);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const teamChoice = verifiedTeam || 'home';
|
|
559
|
+
|
|
560
|
+
// Determine which fields to update
|
|
561
|
+
const teamField = teamChoice === 'home' ? 'home_team_players'
|
|
562
|
+
: teamChoice === 'away' ? 'away_team_players'
|
|
563
|
+
: 'draw_team_players';
|
|
564
|
+
|
|
565
|
+
const poolField = teamChoice === 'home' ? 'home_pool'
|
|
566
|
+
: teamChoice === 'away' ? 'away_pool'
|
|
567
|
+
: 'draw_pool';
|
|
568
|
+
|
|
569
|
+
// Update game with new player (same logic as gamesRoutes join endpoint)
|
|
570
|
+
await pool.query(`
|
|
571
|
+
UPDATE games
|
|
572
|
+
SET ${teamField} = array_append(${teamField}, $1),
|
|
573
|
+
${poolField} = COALESCE(${poolField}, 0) + $3,
|
|
574
|
+
total_pool = COALESCE(total_pool, 0) + $3,
|
|
575
|
+
player_amounts = COALESCE(player_amounts, '{}'::jsonb) || jsonb_build_object($1, $3),
|
|
576
|
+
updated_at = NOW()
|
|
577
|
+
WHERE game_id = $2
|
|
578
|
+
AND NOT ($1 = ANY(COALESCE(home_team_players, ARRAY[]::text[]))
|
|
579
|
+
OR $1 = ANY(COALESCE(away_team_players, ARRAY[]::text[]))
|
|
580
|
+
OR $1 = ANY(COALESCE(draw_team_players, ARRAY[]::text[])))
|
|
581
|
+
`, [wallet, gameId, betAmount]);
|
|
582
|
+
|
|
583
|
+
// Save user's game reference
|
|
584
|
+
await pool.query(`
|
|
585
|
+
INSERT INTO user_game_refs (
|
|
586
|
+
wallet_address, game_id, role, joined_at, team_choice,
|
|
587
|
+
my_signature, my_explorer_url, status, wallet_type
|
|
588
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
|
589
|
+
ON CONFLICT (wallet_address, game_id)
|
|
590
|
+
DO UPDATE SET
|
|
591
|
+
team_choice = EXCLUDED.team_choice,
|
|
592
|
+
my_signature = COALESCE(EXCLUDED.my_signature, user_game_refs.my_signature),
|
|
593
|
+
status = EXCLUDED.status
|
|
594
|
+
`, [
|
|
595
|
+
wallet,
|
|
596
|
+
gameId,
|
|
597
|
+
'player',
|
|
598
|
+
new Date().toISOString(),
|
|
599
|
+
teamChoice,
|
|
600
|
+
signature,
|
|
601
|
+
`https://explorer.solana.com/tx/${signature}${(process.env.SOLANA_NETWORK || '').includes('devnet') ? '?cluster=devnet' : ''}`,
|
|
602
|
+
'active',
|
|
603
|
+
'blink'
|
|
604
|
+
]);
|
|
605
|
+
|
|
606
|
+
console.log(`[Actions] ✅ Database updated for Blink join: ${wallet.slice(0, 8)}... -> ${teamChoice} team`);
|
|
607
|
+
|
|
608
|
+
// Get updated game data for WebSocket broadcast
|
|
609
|
+
const gameResult = await pool.query('SELECT * FROM games WHERE game_id = $1', [gameId]);
|
|
610
|
+
const updatedGame = gameResult.rows[0];
|
|
611
|
+
|
|
612
|
+
// Get user info for notifications
|
|
613
|
+
const userResult = await pool.query('SELECT id, username FROM users WHERE wallet_address = $1', [wallet]);
|
|
614
|
+
const user = userResult.rows[0];
|
|
615
|
+
const joinerUsername = user?.username || `blink_${wallet.slice(0, 6)}`;
|
|
616
|
+
|
|
617
|
+
// Emit WebSocket event to all connected clients
|
|
618
|
+
if (chatNamespace && updatedGame) {
|
|
619
|
+
chatNamespace.emit('game:player_joined', {
|
|
620
|
+
gameId: gameId,
|
|
621
|
+
walletAddress: wallet,
|
|
622
|
+
teamChoice: teamChoice,
|
|
623
|
+
homeTeamCount: updatedGame.home_team_players?.length || 0,
|
|
624
|
+
awayTeamCount: updatedGame.away_team_players?.length || 0,
|
|
625
|
+
drawTeamCount: updatedGame.draw_team_players?.length || 0,
|
|
626
|
+
totalPlayers: (updatedGame.home_team_players?.length || 0) + (updatedGame.away_team_players?.length || 0) + (updatedGame.draw_team_players?.length || 0),
|
|
627
|
+
timestamp: Date.now(),
|
|
628
|
+
source: 'blink'
|
|
629
|
+
});
|
|
630
|
+
console.log(`[Actions] 📡 Broadcasted game:player_joined event`);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Send notifications to other participants
|
|
634
|
+
try {
|
|
635
|
+
const { forwardChatNotification } = require('../services/telegramNotifications');
|
|
636
|
+
|
|
637
|
+
// Get all participants except the joiner
|
|
638
|
+
const allParticipants = [
|
|
639
|
+
...(updatedGame.home_team_players || []),
|
|
640
|
+
...(updatedGame.away_team_players || []),
|
|
641
|
+
...(updatedGame.draw_team_players || []),
|
|
642
|
+
updatedGame.created_by
|
|
643
|
+
].filter(w => w && w !== wallet);
|
|
644
|
+
|
|
645
|
+
const uniqueParticipants = [...new Set(allParticipants)];
|
|
646
|
+
|
|
647
|
+
// Get participant user IDs
|
|
648
|
+
if (uniqueParticipants.length > 0) {
|
|
649
|
+
const participantsResult = await pool.query(
|
|
650
|
+
'SELECT id, wallet_address FROM users WHERE wallet_address = ANY($1)',
|
|
651
|
+
[uniqueParticipants]
|
|
652
|
+
);
|
|
653
|
+
|
|
654
|
+
for (const participant of participantsResult.rows) {
|
|
655
|
+
const notification = {
|
|
656
|
+
type: 'player_joined',
|
|
657
|
+
title: '🎮 New Player Joined!',
|
|
658
|
+
body: `${joinerUsername} joined your bet with ${betAmount} SOL`,
|
|
659
|
+
gameId: gameId,
|
|
660
|
+
data: { gameId, joinerUsername, teamChoice, betAmount }
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
await forwardChatNotification(participant.id, notification).catch(err => {
|
|
664
|
+
console.log(`[Actions] Notification to ${participant.wallet_address.slice(0, 8)} failed:`, err.message);
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
console.log(`[Actions] 📬 Sent notifications to ${participantsResult.rows.length} participant(s)`);
|
|
668
|
+
}
|
|
669
|
+
} catch (notifError) {
|
|
670
|
+
console.error('[Actions] Notification error (non-fatal):', notifError.message);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Return a completed action response with all required fields
|
|
674
|
+
res.json({
|
|
675
|
+
type: 'completed',
|
|
676
|
+
icon: 'https://dubs.app/logo.png',
|
|
677
|
+
title: 'Bet Placed!',
|
|
678
|
+
description: `Successfully bet ${betAmount} SOL on ${teamChoice}`,
|
|
679
|
+
label: 'Complete'
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
} catch (error) {
|
|
683
|
+
console.error('[Actions] Confirm error:', error);
|
|
684
|
+
res.status(500).json({
|
|
685
|
+
type: 'action',
|
|
686
|
+
icon: 'https://dubs.app/logo.png',
|
|
687
|
+
title: 'Error',
|
|
688
|
+
description: error.message || 'Failed to confirm join',
|
|
689
|
+
label: 'Error',
|
|
690
|
+
disabled: true,
|
|
691
|
+
error: { message: error.message || 'Failed to confirm join' }
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
return router;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
module.exports = createActionsRoutes;
|