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,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connect4 Notification Service
|
|
3
|
+
* Handles in-database notifications and Telegram forwarding for Connect4 games
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { forwardChatNotification } = require('./telegramNotifications');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Send "Your Turn" notification to the opponent after a move
|
|
10
|
+
* @param {object} pool - Database pool
|
|
11
|
+
* @param {object} chatNamespace - Socket.io chat namespace
|
|
12
|
+
* @param {object} params - Notification parameters
|
|
13
|
+
* @param {string} params.gameId - Connect4 game ID
|
|
14
|
+
* @param {string} params.opponentWallet - Wallet address of opponent (who needs to move)
|
|
15
|
+
* @param {string} params.moverWallet - Wallet address of player who just moved
|
|
16
|
+
* @param {string} params.moverUsername - Username of player who just moved
|
|
17
|
+
* @param {number} params.buyIn - Game buy-in amount
|
|
18
|
+
*/
|
|
19
|
+
async function notifyYourTurn(pool, chatNamespace, params) {
|
|
20
|
+
const { gameId, opponentWallet, moverWallet, moverUsername, buyIn } = params;
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
// Look up opponent's user ID
|
|
24
|
+
const userResult = await pool.query(
|
|
25
|
+
'SELECT id, username FROM users WHERE wallet_address = $1',
|
|
26
|
+
[opponentWallet]
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
if (userResult.rows.length === 0) {
|
|
30
|
+
console.log(`[Connect4Notif] Opponent ${opponentWallet.slice(0, 8)} not found in users table - skipping notification`);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const opponentUserId = userResult.rows[0].id;
|
|
35
|
+
const opponentUsername = userResult.rows[0].username;
|
|
36
|
+
|
|
37
|
+
// Look up mover's user ID
|
|
38
|
+
const moverResult = await pool.query(
|
|
39
|
+
'SELECT id FROM users WHERE wallet_address = $1',
|
|
40
|
+
[moverWallet]
|
|
41
|
+
);
|
|
42
|
+
const moverUserId = moverResult.rows.length > 0 ? moverResult.rows[0].id : null;
|
|
43
|
+
|
|
44
|
+
// Build notification data with gameInvite structure (frontend expects this)
|
|
45
|
+
const notificationData = {
|
|
46
|
+
gameInvite: {
|
|
47
|
+
gameId,
|
|
48
|
+
gameAddress: '', // Not needed for notification display
|
|
49
|
+
title: '4 the Pot',
|
|
50
|
+
gameType: 'connect4',
|
|
51
|
+
buyIn,
|
|
52
|
+
creatorUsername: moverUsername,
|
|
53
|
+
creatorWallet: moverWallet,
|
|
54
|
+
status: 'playing',
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Insert notification into database
|
|
59
|
+
const insertResult = await pool.query(
|
|
60
|
+
`INSERT INTO chat_notifications (
|
|
61
|
+
user_id, sender_user_id, notification_type, notification_data, read, created_at
|
|
62
|
+
) VALUES ($1, $2, 'connect4_your_turn', $3, false, NOW())
|
|
63
|
+
RETURNING id, created_at`,
|
|
64
|
+
[opponentUserId, moverUserId, JSON.stringify(notificationData)]
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const notificationId = insertResult.rows[0].id;
|
|
68
|
+
const notificationCreatedAt = insertResult.rows[0].created_at;
|
|
69
|
+
|
|
70
|
+
console.log(`[Connect4Notif] Sent connect4_your_turn notification to ${opponentUsername} (ID: ${notificationId})`);
|
|
71
|
+
|
|
72
|
+
// Send real-time notification via WebSocket
|
|
73
|
+
if (chatNamespace) {
|
|
74
|
+
const notification = {
|
|
75
|
+
id: notificationId,
|
|
76
|
+
type: 'connect4_your_turn',
|
|
77
|
+
senderUsername: moverUsername,
|
|
78
|
+
senderWallet: moverWallet,
|
|
79
|
+
message: '',
|
|
80
|
+
gameInvite: notificationData.gameInvite,
|
|
81
|
+
createdAt: notificationCreatedAt.toISOString(),
|
|
82
|
+
read: false,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
chatNamespace.to(`user-${opponentUserId}`).emit('notification', notification);
|
|
86
|
+
console.log(`[Connect4Notif] Real-time notification sent to user-${opponentUserId}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Forward to Telegram
|
|
90
|
+
try {
|
|
91
|
+
const message = `${moverUsername} made a move - it's your turn!`;
|
|
92
|
+
await forwardChatNotification(pool, opponentUserId, 'connect4_your_turn', moverUsername, message, { gameId });
|
|
93
|
+
} catch (telegramError) {
|
|
94
|
+
console.log(`[Connect4Notif] Telegram forward failed:`, telegramError.message);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error('[Connect4Notif] Error sending your turn notification:', error.message);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Send game result notifications (win/lose) to both players
|
|
104
|
+
* @param {object} pool - Database pool
|
|
105
|
+
* @param {object} chatNamespace - Socket.io chat namespace
|
|
106
|
+
* @param {object} params - Notification parameters
|
|
107
|
+
* @param {string} params.gameId - Connect4 game ID
|
|
108
|
+
* @param {string} params.winnerWallet - Winner's wallet address (null for draw)
|
|
109
|
+
* @param {string} params.winnerUsername - Winner's username
|
|
110
|
+
* @param {string} params.loserWallet - Loser's wallet address (null for draw)
|
|
111
|
+
* @param {string} params.loserUsername - Loser's username
|
|
112
|
+
* @param {number} params.buyIn - Game buy-in amount
|
|
113
|
+
* @param {number} params.winnerPrize - Prize amount for winner
|
|
114
|
+
* @param {boolean} params.isDraw - Whether the game was a draw
|
|
115
|
+
*/
|
|
116
|
+
async function notifyGameResult(pool, chatNamespace, params) {
|
|
117
|
+
const { gameId, winnerWallet, winnerUsername, loserWallet, loserUsername, buyIn, winnerPrize, isDraw } = params;
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
// Build gameInvite structure for notification display
|
|
121
|
+
const gameInvite = {
|
|
122
|
+
gameId,
|
|
123
|
+
gameAddress: '',
|
|
124
|
+
title: '4 the Pot',
|
|
125
|
+
gameType: 'connect4',
|
|
126
|
+
buyIn,
|
|
127
|
+
status: 'completed',
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// For draws, notify both players with game_lost (they can claim refund)
|
|
131
|
+
if (isDraw) {
|
|
132
|
+
await notifyPlayer(pool, chatNamespace, {
|
|
133
|
+
walletAddress: winnerWallet, // In draw context, this is player1
|
|
134
|
+
notificationType: 'game_lost',
|
|
135
|
+
gameInvite,
|
|
136
|
+
finalScore: { winner: null, homeScore: 0, awayScore: 0 },
|
|
137
|
+
message: 'Game ended in a draw - claim your refund!',
|
|
138
|
+
gameId,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
await notifyPlayer(pool, chatNamespace, {
|
|
142
|
+
walletAddress: loserWallet, // In draw context, this is player2
|
|
143
|
+
notificationType: 'game_lost',
|
|
144
|
+
gameInvite,
|
|
145
|
+
finalScore: { winner: null, homeScore: 0, awayScore: 0 },
|
|
146
|
+
message: 'Game ended in a draw - claim your refund!',
|
|
147
|
+
gameId,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Notify winner
|
|
154
|
+
if (winnerWallet) {
|
|
155
|
+
await notifyPlayer(pool, chatNamespace, {
|
|
156
|
+
walletAddress: winnerWallet,
|
|
157
|
+
notificationType: 'game_won',
|
|
158
|
+
gameInvite,
|
|
159
|
+
finalScore: { winner: 'home', homeScore: 1, awayScore: 0 }, // Simplified for Connect4
|
|
160
|
+
message: `You won ${winnerPrize?.toFixed(2) || buyIn * 2 * 0.95} SOL!`,
|
|
161
|
+
gameId,
|
|
162
|
+
senderUsername: loserUsername,
|
|
163
|
+
senderWallet: loserWallet,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Notify loser
|
|
168
|
+
if (loserWallet) {
|
|
169
|
+
await notifyPlayer(pool, chatNamespace, {
|
|
170
|
+
walletAddress: loserWallet,
|
|
171
|
+
notificationType: 'game_lost',
|
|
172
|
+
gameInvite,
|
|
173
|
+
finalScore: { winner: 'away', homeScore: 0, awayScore: 1 },
|
|
174
|
+
message: 'Better luck next time!',
|
|
175
|
+
gameId,
|
|
176
|
+
senderUsername: winnerUsername,
|
|
177
|
+
senderWallet: winnerWallet,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
} catch (error) {
|
|
182
|
+
console.error('[Connect4Notif] Error sending game result notifications:', error.message);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Helper to notify a single player
|
|
188
|
+
*/
|
|
189
|
+
async function notifyPlayer(pool, chatNamespace, params) {
|
|
190
|
+
const { walletAddress, notificationType, gameInvite, finalScore, message, gameId, senderUsername, senderWallet } = params;
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
// Look up user
|
|
194
|
+
const userResult = await pool.query(
|
|
195
|
+
'SELECT id, username FROM users WHERE wallet_address = $1',
|
|
196
|
+
[walletAddress]
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
if (userResult.rows.length === 0) {
|
|
200
|
+
console.log(`[Connect4Notif] User ${walletAddress.slice(0, 8)} not found - skipping`);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const userId = userResult.rows[0].id;
|
|
205
|
+
const username = userResult.rows[0].username;
|
|
206
|
+
|
|
207
|
+
// Look up sender's user ID if provided
|
|
208
|
+
let senderUserId = null;
|
|
209
|
+
if (senderWallet) {
|
|
210
|
+
const senderResult = await pool.query(
|
|
211
|
+
'SELECT id FROM users WHERE wallet_address = $1',
|
|
212
|
+
[senderWallet]
|
|
213
|
+
);
|
|
214
|
+
senderUserId = senderResult.rows.length > 0 ? senderResult.rows[0].id : null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Build notification data
|
|
218
|
+
const notificationData = {
|
|
219
|
+
gameInvite,
|
|
220
|
+
finalScore,
|
|
221
|
+
message,
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// Insert notification
|
|
225
|
+
const insertResult = await pool.query(
|
|
226
|
+
`INSERT INTO chat_notifications (
|
|
227
|
+
user_id, sender_user_id, notification_type, notification_data, read, created_at
|
|
228
|
+
) VALUES ($1, $2, $3, $4, false, NOW())
|
|
229
|
+
RETURNING id, created_at`,
|
|
230
|
+
[userId, senderUserId, notificationType, JSON.stringify(notificationData)]
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const notificationId = insertResult.rows[0].id;
|
|
234
|
+
const notificationCreatedAt = insertResult.rows[0].created_at;
|
|
235
|
+
|
|
236
|
+
console.log(`[Connect4Notif] Sent ${notificationType} notification to ${username} (ID: ${notificationId})`);
|
|
237
|
+
|
|
238
|
+
// Send real-time notification via WebSocket
|
|
239
|
+
if (chatNamespace) {
|
|
240
|
+
const notification = {
|
|
241
|
+
id: notificationId,
|
|
242
|
+
type: notificationType,
|
|
243
|
+
senderUsername: senderUsername || '4 the Pot',
|
|
244
|
+
senderWallet: senderWallet || '',
|
|
245
|
+
message: message || '',
|
|
246
|
+
gameInvite,
|
|
247
|
+
finalScore,
|
|
248
|
+
createdAt: notificationCreatedAt.toISOString(),
|
|
249
|
+
read: false,
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
chatNamespace.to(`user-${userId}`).emit('notification', notification);
|
|
253
|
+
console.log(`[Connect4Notif] Real-time ${notificationType} sent to user-${userId}`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Forward to Telegram
|
|
257
|
+
try {
|
|
258
|
+
await forwardChatNotification(pool, userId, notificationType, senderUsername || '4 the Pot', message, { gameId });
|
|
259
|
+
} catch (telegramError) {
|
|
260
|
+
console.log(`[Connect4Notif] Telegram forward failed for ${username}:`, telegramError.message);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
} catch (error) {
|
|
264
|
+
console.error(`[Connect4Notif] Error notifying player ${walletAddress.slice(0, 8)}:`, error.message);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Send cancellation notification to the invited player when creator cancels
|
|
270
|
+
* @param {object} pool - Database pool
|
|
271
|
+
* @param {object} chatNamespace - Socket.io chat namespace
|
|
272
|
+
* @param {object} params - Notification parameters
|
|
273
|
+
* @param {string} params.gameId - Connect4 game ID
|
|
274
|
+
* @param {string} params.invitedPlayerWallet - Wallet address of invited player
|
|
275
|
+
* @param {string} params.creatorWallet - Creator's wallet address
|
|
276
|
+
* @param {string} params.creatorUsername - Creator's username
|
|
277
|
+
* @param {number} params.buyIn - Game buy-in amount
|
|
278
|
+
*/
|
|
279
|
+
async function notifyGameCancelled(pool, chatNamespace, params) {
|
|
280
|
+
const { gameId, invitedPlayerWallet, creatorWallet, creatorUsername, buyIn } = params;
|
|
281
|
+
|
|
282
|
+
if (!invitedPlayerWallet) {
|
|
283
|
+
console.log(`[Connect4Notif] No invited player for game ${gameId} - skipping cancel notification`);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
// Look up invited player's user ID
|
|
289
|
+
const userResult = await pool.query(
|
|
290
|
+
'SELECT id, username FROM users WHERE wallet_address = $1',
|
|
291
|
+
[invitedPlayerWallet]
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
if (userResult.rows.length === 0) {
|
|
295
|
+
console.log(`[Connect4Notif] Invited player ${invitedPlayerWallet.slice(0, 8)} not found - skipping`);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const invitedUserId = userResult.rows[0].id;
|
|
300
|
+
const invitedUsername = userResult.rows[0].username;
|
|
301
|
+
|
|
302
|
+
// Look up creator's user ID and avatar
|
|
303
|
+
const creatorResult = await pool.query(
|
|
304
|
+
'SELECT id, avatar FROM users WHERE wallet_address = $1',
|
|
305
|
+
[creatorWallet]
|
|
306
|
+
);
|
|
307
|
+
const creatorUserId = creatorResult.rows.length > 0 ? creatorResult.rows[0].id : null;
|
|
308
|
+
const creatorAvatar = creatorResult.rows.length > 0 ? creatorResult.rows[0].avatar : null;
|
|
309
|
+
|
|
310
|
+
// Build notification data with gameInvite structure
|
|
311
|
+
const notificationData = {
|
|
312
|
+
gameInvite: {
|
|
313
|
+
gameId,
|
|
314
|
+
gameAddress: '',
|
|
315
|
+
title: '4 the Pot',
|
|
316
|
+
gameType: 'connect4',
|
|
317
|
+
buyIn,
|
|
318
|
+
creatorUsername,
|
|
319
|
+
creatorWallet,
|
|
320
|
+
status: 'cancelled',
|
|
321
|
+
},
|
|
322
|
+
message: `${creatorUsername} cancelled the game`,
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
// Insert notification into database
|
|
326
|
+
const insertResult = await pool.query(
|
|
327
|
+
`INSERT INTO chat_notifications (
|
|
328
|
+
user_id, sender_user_id, notification_type, notification_data, read, created_at
|
|
329
|
+
) VALUES ($1, $2, 'game_cancelled', $3, false, NOW())
|
|
330
|
+
RETURNING id, created_at`,
|
|
331
|
+
[invitedUserId, creatorUserId, JSON.stringify(notificationData)]
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
const notificationId = insertResult.rows[0].id;
|
|
335
|
+
const notificationCreatedAt = insertResult.rows[0].created_at;
|
|
336
|
+
|
|
337
|
+
console.log(`[Connect4Notif] Sent game_cancelled notification to ${invitedUsername} (ID: ${notificationId})`);
|
|
338
|
+
|
|
339
|
+
// Send real-time notification via WebSocket
|
|
340
|
+
if (chatNamespace) {
|
|
341
|
+
const notification = {
|
|
342
|
+
id: notificationId,
|
|
343
|
+
type: 'game_cancelled',
|
|
344
|
+
senderUsername: creatorUsername,
|
|
345
|
+
senderWallet: creatorWallet,
|
|
346
|
+
senderAvatar: creatorAvatar,
|
|
347
|
+
message: `${creatorUsername} cancelled the game`,
|
|
348
|
+
gameInvite: notificationData.gameInvite,
|
|
349
|
+
createdAt: notificationCreatedAt.toISOString(),
|
|
350
|
+
read: false,
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
chatNamespace.to(`user-${invitedUserId}`).emit('notification', notification);
|
|
354
|
+
console.log(`[Connect4Notif] Real-time cancel notification sent to user-${invitedUserId}`);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Forward to Telegram
|
|
358
|
+
try {
|
|
359
|
+
const message = `${creatorUsername} cancelled the Connect 4 game invite`;
|
|
360
|
+
await forwardChatNotification(pool, invitedUserId, 'game_lost', creatorUsername, message, { gameId });
|
|
361
|
+
} catch (telegramError) {
|
|
362
|
+
console.log(`[Connect4Notif] Telegram forward failed:`, telegramError.message);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
} catch (error) {
|
|
366
|
+
console.error('[Connect4Notif] Error sending cancel notification:', error.message);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
module.exports = {
|
|
371
|
+
notifyYourTurn,
|
|
372
|
+
notifyGameResult,
|
|
373
|
+
notifyGameCancelled,
|
|
374
|
+
};
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
const { pool } = require('./db'); // Shared database pool
|
|
3
|
+
|
|
4
|
+
class CryptoPriceService {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.coingeckoApiUrl = 'https://api.coingecko.com/api/v3';
|
|
7
|
+
this.cacheTTL = parseInt(process.env.CRYPTO_PRICE_CACHE_TTL) || 300; // 5 minutes in seconds
|
|
8
|
+
this.supportedCryptos = ['solana']; // CoinGecko IDs
|
|
9
|
+
|
|
10
|
+
// Initialize cache table on startup
|
|
11
|
+
this.initializeCacheTable();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Initialize the crypto_prices_cache table if it doesn't exist
|
|
16
|
+
*/
|
|
17
|
+
async initializeCacheTable() {
|
|
18
|
+
try {
|
|
19
|
+
await pool.query(`
|
|
20
|
+
CREATE TABLE IF NOT EXISTS crypto_prices_cache (
|
|
21
|
+
id SERIAL PRIMARY KEY,
|
|
22
|
+
crypto_id VARCHAR(50) NOT NULL UNIQUE,
|
|
23
|
+
prices JSONB NOT NULL,
|
|
24
|
+
last_updated TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
25
|
+
expires_at TIMESTAMP NOT NULL,
|
|
26
|
+
created_at TIMESTAMP DEFAULT NOW()
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
CREATE INDEX IF NOT EXISTS idx_crypto_prices_id ON crypto_prices_cache(crypto_id);
|
|
30
|
+
CREATE INDEX IF NOT EXISTS idx_crypto_prices_expires ON crypto_prices_cache(expires_at);
|
|
31
|
+
`);
|
|
32
|
+
console.log('[Crypto Prices] Cache table initialized');
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error('[Crypto Prices] Error initializing cache table:', error);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get crypto price in multiple fiat currencies
|
|
40
|
+
* @param {string} cryptoId - CoinGecko crypto ID (e.g., 'solana')
|
|
41
|
+
* @param {string[]} vsCurrencies - Array of currency codes to get prices in
|
|
42
|
+
* @returns {Object} Prices object
|
|
43
|
+
*/
|
|
44
|
+
async getCryptoPrice(cryptoId = 'solana', vsCurrencies = ['usd', 'eur', 'gbp', 'cad', 'jpy', 'aud']) {
|
|
45
|
+
try {
|
|
46
|
+
// Try to get from cache first
|
|
47
|
+
const cachedPrice = await this.getCachedPrice(cryptoId);
|
|
48
|
+
if (cachedPrice) {
|
|
49
|
+
console.log(`[Crypto Prices] Retrieved ${cryptoId} from cache`);
|
|
50
|
+
return {
|
|
51
|
+
...cachedPrice,
|
|
52
|
+
source: 'cache'
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// If not in cache, fetch from CoinGecko
|
|
57
|
+
console.log(`[Crypto Prices] Fetching fresh price for ${cryptoId} from CoinGecko`);
|
|
58
|
+
const freshPrice = await this.fetchFromCoinGecko(cryptoId, vsCurrencies);
|
|
59
|
+
|
|
60
|
+
// Cache the fresh price
|
|
61
|
+
await this.cachePrice(cryptoId, freshPrice);
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
...freshPrice,
|
|
65
|
+
source: 'api'
|
|
66
|
+
};
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error('[Crypto Prices] Error getting crypto price:', error);
|
|
69
|
+
throw new Error('Failed to retrieve crypto price');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get SOL price in USD (convenience method)
|
|
75
|
+
* @returns {number} SOL price in USD
|
|
76
|
+
*/
|
|
77
|
+
async getSOLPriceUSD() {
|
|
78
|
+
const priceData = await this.getCryptoPrice('solana', ['usd']);
|
|
79
|
+
return priceData.prices.usd;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get cached crypto price from PostgreSQL
|
|
84
|
+
* @param {string} cryptoId - Crypto ID
|
|
85
|
+
* @returns {Object|null} Cached price or null
|
|
86
|
+
*/
|
|
87
|
+
async getCachedPrice(cryptoId) {
|
|
88
|
+
try {
|
|
89
|
+
const result = await pool.query(
|
|
90
|
+
'SELECT prices, last_updated FROM crypto_prices_cache WHERE crypto_id = $1 AND expires_at > NOW()',
|
|
91
|
+
[cryptoId]
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
if (result.rows.length > 0) {
|
|
95
|
+
const row = result.rows[0];
|
|
96
|
+
return {
|
|
97
|
+
cryptoId,
|
|
98
|
+
prices: row.prices,
|
|
99
|
+
lastUpdated: row.last_updated,
|
|
100
|
+
timestamp: new Date(row.last_updated).getTime()
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return null;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.error('[Crypto Prices] Error getting cached price:', error);
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Cache crypto price in PostgreSQL
|
|
113
|
+
* @param {string} cryptoId - Crypto ID
|
|
114
|
+
* @param {Object} priceData - Price data
|
|
115
|
+
*/
|
|
116
|
+
async cachePrice(cryptoId, priceData) {
|
|
117
|
+
try {
|
|
118
|
+
const expiresAt = new Date(Date.now() + this.cacheTTL * 1000);
|
|
119
|
+
|
|
120
|
+
await pool.query(`
|
|
121
|
+
INSERT INTO crypto_prices_cache (crypto_id, prices, last_updated, expires_at)
|
|
122
|
+
VALUES ($1, $2, NOW(), $3)
|
|
123
|
+
ON CONFLICT (crypto_id)
|
|
124
|
+
DO UPDATE SET
|
|
125
|
+
prices = $2,
|
|
126
|
+
last_updated = NOW(),
|
|
127
|
+
expires_at = $3
|
|
128
|
+
`, [cryptoId, JSON.stringify(priceData.prices), expiresAt]);
|
|
129
|
+
|
|
130
|
+
console.log(`[Crypto Prices] Cached ${cryptoId} prices with TTL: ${this.cacheTTL}s`);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
console.error('[Crypto Prices] Error caching price:', error);
|
|
133
|
+
// Don't throw error, caching failure shouldn't break the service
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Fetch crypto prices from CoinGecko API
|
|
139
|
+
* @param {string} cryptoId - CoinGecko crypto ID
|
|
140
|
+
* @param {string[]} vsCurrencies - Array of currency codes
|
|
141
|
+
* @returns {Object} Fresh crypto prices
|
|
142
|
+
*/
|
|
143
|
+
async fetchFromCoinGecko(cryptoId, vsCurrencies) {
|
|
144
|
+
try {
|
|
145
|
+
const currenciesParam = vsCurrencies.join(',');
|
|
146
|
+
const url = `${this.coingeckoApiUrl}/simple/price?ids=${cryptoId}&vs_currencies=${currenciesParam}`;
|
|
147
|
+
|
|
148
|
+
const response = await axios.get(url, {
|
|
149
|
+
timeout: 10000, // 10 second timeout
|
|
150
|
+
headers: {
|
|
151
|
+
'User-Agent': 'Dubs-Crypto-Price-API/1.0',
|
|
152
|
+
'Accept': 'application/json'
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
if (!response.data || !response.data[cryptoId]) {
|
|
157
|
+
throw new Error('Invalid response from CoinGecko API');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const prices = response.data[cryptoId];
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
cryptoId,
|
|
164
|
+
prices,
|
|
165
|
+
timestamp: Date.now(),
|
|
166
|
+
lastUpdated: new Date().toISOString()
|
|
167
|
+
};
|
|
168
|
+
} catch (error) {
|
|
169
|
+
if (error.response) {
|
|
170
|
+
console.error('[Crypto Prices] CoinGecko API Error:', error.response.status, error.response.data);
|
|
171
|
+
|
|
172
|
+
// Handle specific API errors
|
|
173
|
+
if (error.response.status === 429) {
|
|
174
|
+
throw new Error('CoinGecko API: Rate limit exceeded');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
throw new Error(`CoinGecko API error: ${error.response.status}`);
|
|
178
|
+
} else if (error.request) {
|
|
179
|
+
console.error('[Crypto Prices] Network Error:', error.message);
|
|
180
|
+
throw new Error('Network error connecting to CoinGecko API');
|
|
181
|
+
} else {
|
|
182
|
+
console.error('[Crypto Prices] Error:', error.message);
|
|
183
|
+
throw new Error('Failed to fetch crypto prices');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Clear cache for a specific crypto
|
|
190
|
+
* @param {string} cryptoId - Crypto ID
|
|
191
|
+
*/
|
|
192
|
+
async clearCache(cryptoId = 'solana') {
|
|
193
|
+
try {
|
|
194
|
+
await pool.query(
|
|
195
|
+
'DELETE FROM crypto_prices_cache WHERE crypto_id = $1',
|
|
196
|
+
[cryptoId]
|
|
197
|
+
);
|
|
198
|
+
console.log(`[Crypto Prices] Cache cleared for ${cryptoId}`);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
console.error('[Crypto Prices] Error clearing cache:', error);
|
|
201
|
+
throw new Error('Failed to clear cache');
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Clean up expired cache entries
|
|
207
|
+
*/
|
|
208
|
+
async cleanupExpiredCache() {
|
|
209
|
+
try {
|
|
210
|
+
const result = await pool.query(
|
|
211
|
+
'DELETE FROM crypto_prices_cache WHERE expires_at < NOW()'
|
|
212
|
+
);
|
|
213
|
+
if (result.rowCount > 0) {
|
|
214
|
+
console.log(`[Crypto Prices] Cleaned up ${result.rowCount} expired cache entries`);
|
|
215
|
+
}
|
|
216
|
+
} catch (error) {
|
|
217
|
+
console.error('[Crypto Prices] Error cleaning up cache:', error);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
module.exports = new CryptoPriceService();
|
|
223
|
+
|