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,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Portfolio API Routes
|
|
3
|
+
*
|
|
4
|
+
* Public endpoints for fetching Solana wallet portfolios (SOL + SPL tokens).
|
|
5
|
+
* Data is fetched directly from Solana RPC with 30-second caching.
|
|
6
|
+
* Also includes admin endpoints for browsing all users' portfolios.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const express = require('express');
|
|
10
|
+
const router = express.Router();
|
|
11
|
+
const redisService = require('../services/redisService');
|
|
12
|
+
|
|
13
|
+
// Cache key for users list
|
|
14
|
+
const USERS_CACHE_KEY = 'portfolio:users:list';
|
|
15
|
+
const USERS_CACHE_TTL = 60; // 1 minute TTL for users list
|
|
16
|
+
|
|
17
|
+
module.exports = (portfolioService, dbPool) => {
|
|
18
|
+
/**
|
|
19
|
+
* GET /api/portfolio/health
|
|
20
|
+
* Health check endpoint for the portfolio service
|
|
21
|
+
* NOTE: Must be defined BEFORE /:walletAddress to avoid route collision
|
|
22
|
+
*/
|
|
23
|
+
router.get('/health', async (req, res) => {
|
|
24
|
+
const stats = portfolioService.getCacheStats();
|
|
25
|
+
res.json({
|
|
26
|
+
success: true,
|
|
27
|
+
status: 'healthy',
|
|
28
|
+
cache: stats,
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* GET /api/portfolio/users
|
|
34
|
+
* Get list of all users with wallet addresses (for admin portfolio browsing)
|
|
35
|
+
* Returns: id, wallet_address, username, avatar
|
|
36
|
+
* Cached in Redis for 1 minute
|
|
37
|
+
* NOTE: Must be defined BEFORE /:walletAddress to avoid route collision
|
|
38
|
+
*/
|
|
39
|
+
router.get('/users', async (req, res) => {
|
|
40
|
+
try {
|
|
41
|
+
// Check Redis cache first
|
|
42
|
+
if (redisService.isAvailable()) {
|
|
43
|
+
try {
|
|
44
|
+
const cached = await redisService.get(USERS_CACHE_KEY);
|
|
45
|
+
if (cached) {
|
|
46
|
+
const data = JSON.parse(cached);
|
|
47
|
+
return res.json({
|
|
48
|
+
success: true,
|
|
49
|
+
data: data.users,
|
|
50
|
+
totalUsers: data.totalUsers,
|
|
51
|
+
fromCache: true,
|
|
52
|
+
cachedAt: data.cachedAt,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
} catch (e) {
|
|
56
|
+
console.error('[Portfolio] Redis cache read error:', e.message);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Fetch from database
|
|
61
|
+
const result = await dbPool.query(`
|
|
62
|
+
SELECT
|
|
63
|
+
id,
|
|
64
|
+
wallet_address,
|
|
65
|
+
username,
|
|
66
|
+
avatar,
|
|
67
|
+
created_at
|
|
68
|
+
FROM users
|
|
69
|
+
WHERE wallet_address IS NOT NULL
|
|
70
|
+
ORDER BY created_at DESC
|
|
71
|
+
`);
|
|
72
|
+
|
|
73
|
+
const users = result.rows.map(row => ({
|
|
74
|
+
id: row.id,
|
|
75
|
+
walletAddress: row.wallet_address,
|
|
76
|
+
username: row.username || null,
|
|
77
|
+
avatar: row.avatar || null,
|
|
78
|
+
createdAt: row.created_at,
|
|
79
|
+
}));
|
|
80
|
+
|
|
81
|
+
const cacheData = {
|
|
82
|
+
users,
|
|
83
|
+
totalUsers: users.length,
|
|
84
|
+
cachedAt: new Date().toISOString(),
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// Cache in Redis
|
|
88
|
+
if (redisService.isAvailable()) {
|
|
89
|
+
try {
|
|
90
|
+
await redisService.set(USERS_CACHE_KEY, JSON.stringify(cacheData), USERS_CACHE_TTL);
|
|
91
|
+
} catch (e) {
|
|
92
|
+
console.error('[Portfolio] Redis cache write error:', e.message);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
res.json({
|
|
97
|
+
success: true,
|
|
98
|
+
data: users,
|
|
99
|
+
totalUsers: users.length,
|
|
100
|
+
fromCache: false,
|
|
101
|
+
});
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.error('[Portfolio] Error fetching users:', error.message);
|
|
104
|
+
res.status(500).json({
|
|
105
|
+
success: false,
|
|
106
|
+
error: 'Failed to fetch users list',
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* GET /api/portfolio/:walletAddress
|
|
113
|
+
* Get full portfolio for a wallet (SOL balance + all SPL tokens)
|
|
114
|
+
*
|
|
115
|
+
* Query params:
|
|
116
|
+
* - network: 'mainnet-beta' | 'devnet' (default: mainnet-beta)
|
|
117
|
+
*
|
|
118
|
+
* Response:
|
|
119
|
+
* {
|
|
120
|
+
* success: true,
|
|
121
|
+
* data: {
|
|
122
|
+
* walletAddress: "8syiXBXMF55...",
|
|
123
|
+
* network: "mainnet-beta",
|
|
124
|
+
* nativeBalance: { symbol: "SOL", balance: 1.5, ... },
|
|
125
|
+
* tokens: [...],
|
|
126
|
+
* totalTokens: 5,
|
|
127
|
+
* cachedAt: "2026-02-01T...",
|
|
128
|
+
* fromCache: true/false
|
|
129
|
+
* }
|
|
130
|
+
* }
|
|
131
|
+
*/
|
|
132
|
+
router.get('/:walletAddress', async (req, res) => {
|
|
133
|
+
try {
|
|
134
|
+
const { walletAddress } = req.params;
|
|
135
|
+
const network = req.query.network || 'mainnet-beta';
|
|
136
|
+
|
|
137
|
+
// Validate network parameter
|
|
138
|
+
const validNetworks = ['mainnet-beta', 'devnet'];
|
|
139
|
+
if (!validNetworks.includes(network)) {
|
|
140
|
+
return res.status(400).json({
|
|
141
|
+
success: false,
|
|
142
|
+
error: `Invalid network. Must be one of: ${validNetworks.join(', ')}`,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const portfolio = await portfolioService.getPortfolio(walletAddress, network, false);
|
|
147
|
+
|
|
148
|
+
res.json({
|
|
149
|
+
success: true,
|
|
150
|
+
data: portfolio,
|
|
151
|
+
});
|
|
152
|
+
} catch (error) {
|
|
153
|
+
console.error('[Portfolio] Error:', error.message);
|
|
154
|
+
|
|
155
|
+
// Handle specific error types
|
|
156
|
+
if (error.message === 'Invalid wallet address') {
|
|
157
|
+
return res.status(400).json({
|
|
158
|
+
success: false,
|
|
159
|
+
error: 'Invalid wallet address format',
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
res.status(500).json({
|
|
164
|
+
success: false,
|
|
165
|
+
error: error.message || 'Failed to fetch portfolio',
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* GET /api/portfolio/:walletAddress/refresh
|
|
172
|
+
* Force cache refresh and return fresh portfolio data
|
|
173
|
+
*
|
|
174
|
+
* Query params:
|
|
175
|
+
* - network: 'mainnet-beta' | 'devnet' (default: mainnet-beta)
|
|
176
|
+
*/
|
|
177
|
+
router.get('/:walletAddress/refresh', async (req, res) => {
|
|
178
|
+
try {
|
|
179
|
+
const { walletAddress } = req.params;
|
|
180
|
+
const network = req.query.network || 'mainnet-beta';
|
|
181
|
+
|
|
182
|
+
// Validate network parameter
|
|
183
|
+
const validNetworks = ['mainnet-beta', 'devnet'];
|
|
184
|
+
if (!validNetworks.includes(network)) {
|
|
185
|
+
return res.status(400).json({
|
|
186
|
+
success: false,
|
|
187
|
+
error: `Invalid network. Must be one of: ${validNetworks.join(', ')}`,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Force refresh by passing true
|
|
192
|
+
const portfolio = await portfolioService.getPortfolio(walletAddress, network, true);
|
|
193
|
+
|
|
194
|
+
res.json({
|
|
195
|
+
success: true,
|
|
196
|
+
data: portfolio,
|
|
197
|
+
refreshed: true,
|
|
198
|
+
});
|
|
199
|
+
} catch (error) {
|
|
200
|
+
console.error('[Portfolio] Refresh error:', error.message);
|
|
201
|
+
|
|
202
|
+
if (error.message === 'Invalid wallet address') {
|
|
203
|
+
return res.status(400).json({
|
|
204
|
+
success: false,
|
|
205
|
+
error: 'Invalid wallet address format',
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
res.status(500).json({
|
|
210
|
+
success: false,
|
|
211
|
+
error: error.message || 'Failed to refresh portfolio',
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
return router;
|
|
217
|
+
};
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Promo Routes - Handle promo code validation and sponsored game joins
|
|
3
|
+
*
|
|
4
|
+
* Endpoints:
|
|
5
|
+
* POST /promo/validate - Validate a promo code (check if available)
|
|
6
|
+
* POST /promo/reserve - Reserve a promo code for a user
|
|
7
|
+
* GET /promo/active - Get user's active promo code
|
|
8
|
+
* GET /promo/status - Get treasury status (admin)
|
|
9
|
+
* GET /promo/stats - Get promo code statistics (admin)
|
|
10
|
+
* POST /promo/generate - Generate new promo codes (admin)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const express = require('express');
|
|
14
|
+
const router = express.Router();
|
|
15
|
+
const { authenticate } = require('../middleware/authenticate');
|
|
16
|
+
const promoService = require('../services/promoService');
|
|
17
|
+
const promoTreasuryService = require('../services/promoTreasuryService');
|
|
18
|
+
|
|
19
|
+
// Admin wallet addresses that can manage promo codes
|
|
20
|
+
const ADMIN_WALLETS = [
|
|
21
|
+
'Hvv1ctqHLR5wonuuRguefS6EpGUe7tFRBX2YWHGr3mes', // Main admin
|
|
22
|
+
'27MAuzKZT5SfE6iwLJaaBpzcXeD474ybDsJEv4Fp7Cmj',
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Middleware: Require admin wallet for sensitive operations
|
|
27
|
+
*/
|
|
28
|
+
function requireAdmin(req, res, next) {
|
|
29
|
+
if (!req.user || !ADMIN_WALLETS.includes(req.user.walletAddress)) {
|
|
30
|
+
return res.status(403).json({
|
|
31
|
+
success: false,
|
|
32
|
+
error: 'Admin access required',
|
|
33
|
+
code: 'ADMIN_REQUIRED',
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
next();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* POST /promo/validate
|
|
41
|
+
* Validate a promo code (check if it exists and is available)
|
|
42
|
+
* Does NOT reserve the code - just checks validity
|
|
43
|
+
*
|
|
44
|
+
* Body: { code: "DUBS-XXXX-XXXX" }
|
|
45
|
+
* Response: { valid: boolean, amountSOL?: number, error?: string }
|
|
46
|
+
*/
|
|
47
|
+
router.post('/validate', async (req, res) => {
|
|
48
|
+
try {
|
|
49
|
+
const { code } = req.body;
|
|
50
|
+
|
|
51
|
+
if (!code) {
|
|
52
|
+
return res.status(400).json({ valid: false, error: 'Promo code is required' });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const result = await promoService.validateCode(code);
|
|
56
|
+
res.json(result);
|
|
57
|
+
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error('[PromoRoutes] Error validating code:', error.message);
|
|
60
|
+
res.status(500).json({ valid: false, error: 'Server error validating promo code' });
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* POST /promo/reserve
|
|
66
|
+
* Reserve a promo code for a user during onboarding
|
|
67
|
+
* Code is reserved for 30 minutes before expiring
|
|
68
|
+
*
|
|
69
|
+
* Body: { code: "DUBS-XXXX-XXXX", walletAddress: "..." }
|
|
70
|
+
* Response: { success: boolean, amountSOL?: number, expiresAt?: string, error?: string }
|
|
71
|
+
*/
|
|
72
|
+
router.post('/reserve', async (req, res) => {
|
|
73
|
+
try {
|
|
74
|
+
const { code, walletAddress } = req.body;
|
|
75
|
+
|
|
76
|
+
if (!code) {
|
|
77
|
+
return res.status(400).json({ success: false, error: 'Promo code is required' });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!walletAddress) {
|
|
81
|
+
return res.status(400).json({ success: false, error: 'Wallet address is required' });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Check treasury balance before allowing reservation
|
|
85
|
+
const treasuryReady = promoTreasuryService.isReady();
|
|
86
|
+
if (!treasuryReady) {
|
|
87
|
+
return res.status(503).json({
|
|
88
|
+
success: false,
|
|
89
|
+
error: 'Promo system is currently unavailable'
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const result = await promoService.reserveCode(code, walletAddress);
|
|
94
|
+
|
|
95
|
+
if (result.success) {
|
|
96
|
+
// Verify treasury has enough balance for this promo amount
|
|
97
|
+
const balanceCheck = await promoTreasuryService.hasEnoughBalance(result.amountLamports);
|
|
98
|
+
if (!balanceCheck.enough) {
|
|
99
|
+
// Release the reservation - treasury can't cover it
|
|
100
|
+
console.error('[PromoRoutes] Treasury balance insufficient, releasing reservation');
|
|
101
|
+
// Note: We don't have an explicit "release" function, but the reservation
|
|
102
|
+
// will auto-expire. For better UX, we could add one.
|
|
103
|
+
return res.status(503).json({
|
|
104
|
+
success: false,
|
|
105
|
+
error: 'Promo system temporarily unavailable - please try again later'
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
res.json(result);
|
|
111
|
+
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.error('[PromoRoutes] Error reserving code:', error.message);
|
|
114
|
+
res.status(500).json({ success: false, error: 'Server error reserving promo code' });
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* GET /promo/active
|
|
120
|
+
* Get user's currently active (reserved but unused) promo code
|
|
121
|
+
*
|
|
122
|
+
* Query: ?walletAddress=...
|
|
123
|
+
* Response: { code?: string, amountSOL?: number, expiresAt?: string } or null
|
|
124
|
+
*/
|
|
125
|
+
router.get('/active', async (req, res) => {
|
|
126
|
+
try {
|
|
127
|
+
const { walletAddress } = req.query;
|
|
128
|
+
|
|
129
|
+
if (!walletAddress) {
|
|
130
|
+
return res.status(400).json({ error: 'Wallet address is required' });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const activePromo = await promoService.getActivePromoForUser(walletAddress);
|
|
134
|
+
res.json(activePromo || { hasActivePromo: false });
|
|
135
|
+
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.error('[PromoRoutes] Error getting active promo:', error.message);
|
|
138
|
+
res.status(500).json({ error: 'Server error getting active promo' });
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* GET /promo/status
|
|
144
|
+
* Get treasury wallet status (for admin monitoring)
|
|
145
|
+
* PROTECTED: Admin only
|
|
146
|
+
*
|
|
147
|
+
* Response: { initialized: boolean, address?: string, balance?: { sol: number, isLow: boolean } }
|
|
148
|
+
*/
|
|
149
|
+
router.get('/status', authenticate, requireAdmin, async (req, res) => {
|
|
150
|
+
try {
|
|
151
|
+
const status = await promoTreasuryService.getStatus();
|
|
152
|
+
res.json(status);
|
|
153
|
+
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error('[PromoRoutes] Error getting treasury status:', error.message);
|
|
156
|
+
res.status(500).json({ error: 'Server error getting treasury status' });
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* GET /promo/stats
|
|
162
|
+
* Get promo code statistics (for admin dashboard)
|
|
163
|
+
* PROTECTED: Admin only
|
|
164
|
+
*
|
|
165
|
+
* Response: { totalCodes, available, reserved, used, outcomes: { won, lost, refunded }, totalUsedSOL }
|
|
166
|
+
*/
|
|
167
|
+
router.get('/stats', authenticate, requireAdmin, async (req, res) => {
|
|
168
|
+
try {
|
|
169
|
+
const stats = await promoService.getStats();
|
|
170
|
+
res.json(stats);
|
|
171
|
+
|
|
172
|
+
} catch (error) {
|
|
173
|
+
console.error('[PromoRoutes] Error getting stats:', error.message);
|
|
174
|
+
res.status(500).json({ error: 'Server error getting promo stats' });
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* GET /promo/treasury
|
|
180
|
+
* Get treasury wallet balance (for admin dashboard)
|
|
181
|
+
* PROTECTED: Admin only
|
|
182
|
+
*
|
|
183
|
+
* Response: { address, balanceLamports, balanceSOL, network }
|
|
184
|
+
*/
|
|
185
|
+
router.get('/treasury', authenticate, requireAdmin, async (req, res) => {
|
|
186
|
+
try {
|
|
187
|
+
const treasuryInfo = await promoTreasuryService.getTreasuryInfo();
|
|
188
|
+
res.json(treasuryInfo);
|
|
189
|
+
|
|
190
|
+
} catch (error) {
|
|
191
|
+
console.error('[PromoRoutes] Error getting treasury info:', error.message);
|
|
192
|
+
res.status(500).json({ error: 'Server error getting treasury info' });
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* POST /promo/generate
|
|
198
|
+
* Generate new promo codes (admin only)
|
|
199
|
+
* PROTECTED: Admin only
|
|
200
|
+
*
|
|
201
|
+
* Body: { count: number, amountSOL?: number, expiresAt?: string, batchId?: string }
|
|
202
|
+
* Response: { codes: [{ code, amountSOL }], batchId }
|
|
203
|
+
*/
|
|
204
|
+
router.post('/generate', authenticate, requireAdmin, async (req, res) => {
|
|
205
|
+
try {
|
|
206
|
+
const { count = 2, amountSOL, expiresAt, batchId, notes } = req.body;
|
|
207
|
+
|
|
208
|
+
if (count < 1 || count > 100) {
|
|
209
|
+
return res.status(400).json({ error: 'Count must be between 1 and 100' });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const options = {
|
|
213
|
+
batchId: batchId || `twitter-${new Date().toISOString().split('T')[0]}`,
|
|
214
|
+
notes
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
if (amountSOL) {
|
|
218
|
+
options.amountLamports = Math.floor(amountSOL * 1_000_000_000);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (expiresAt) {
|
|
222
|
+
options.expiresAt = new Date(expiresAt);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const codes = await promoService.generateBatch(count, options);
|
|
226
|
+
|
|
227
|
+
res.json({
|
|
228
|
+
success: true,
|
|
229
|
+
batchId: options.batchId,
|
|
230
|
+
count: codes.length,
|
|
231
|
+
codes
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
} catch (error) {
|
|
235
|
+
console.error('[PromoRoutes] Error generating codes:', error.message);
|
|
236
|
+
res.status(500).json({ error: 'Server error generating promo codes' });
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* GET /promo/all
|
|
242
|
+
* Get all promo codes (admin only)
|
|
243
|
+
* PROTECTED: Admin only
|
|
244
|
+
*
|
|
245
|
+
* Response: { codes: [...] }
|
|
246
|
+
*/
|
|
247
|
+
router.get('/all', authenticate, requireAdmin, async (req, res) => {
|
|
248
|
+
try {
|
|
249
|
+
const codes = await promoService.getAllCodes();
|
|
250
|
+
res.json({ codes });
|
|
251
|
+
|
|
252
|
+
} catch (error) {
|
|
253
|
+
console.error('[PromoRoutes] Error getting all codes:', error.message);
|
|
254
|
+
res.status(500).json({ error: 'Server error getting promo codes' });
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* GET /promo/batch/:batchId
|
|
260
|
+
* Get all codes in a batch (for tracking Twitter giveaways)
|
|
261
|
+
* PROTECTED: Admin only
|
|
262
|
+
*
|
|
263
|
+
* Response: { codes: [...] }
|
|
264
|
+
*/
|
|
265
|
+
router.get('/batch/:batchId', authenticate, requireAdmin, async (req, res) => {
|
|
266
|
+
try {
|
|
267
|
+
const { batchId } = req.params;
|
|
268
|
+
|
|
269
|
+
const codes = await promoService.getCodesByBatch(batchId);
|
|
270
|
+
res.json({ batchId, count: codes.length, codes });
|
|
271
|
+
|
|
272
|
+
} catch (error) {
|
|
273
|
+
console.error('[PromoRoutes] Error getting batch:', error.message);
|
|
274
|
+
res.status(500).json({ error: 'Server error getting batch codes' });
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* GET /promo/status/:walletAddress
|
|
280
|
+
* Get user's promo status for the in-app banner
|
|
281
|
+
* Returns info about their reserved or used promo code
|
|
282
|
+
*
|
|
283
|
+
* Response: {
|
|
284
|
+
* hasActivePromo: boolean,
|
|
285
|
+
* code: string | null,
|
|
286
|
+
* amountSOL: number,
|
|
287
|
+
* status: 'reserved' | 'used' | null,
|
|
288
|
+
* gameOutcome: 'won' | 'lost' | 'refunded' | null,
|
|
289
|
+
* bannerDismissed: boolean
|
|
290
|
+
* }
|
|
291
|
+
*/
|
|
292
|
+
router.get('/status/:walletAddress', async (req, res) => {
|
|
293
|
+
try {
|
|
294
|
+
const { walletAddress } = req.params;
|
|
295
|
+
|
|
296
|
+
if (!walletAddress) {
|
|
297
|
+
return res.status(400).json({ error: 'Wallet address is required' });
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const status = await promoService.getUserPromoStatus(walletAddress);
|
|
301
|
+
res.json(status);
|
|
302
|
+
|
|
303
|
+
} catch (error) {
|
|
304
|
+
console.error('[PromoRoutes] Error getting user promo status:', error.message);
|
|
305
|
+
res.status(500).json({ error: 'Server error getting promo status' });
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* POST /promo/release
|
|
311
|
+
* Release a promo code back to available status (admin only)
|
|
312
|
+
* Use this to clean up duplicate reservations
|
|
313
|
+
* PROTECTED: Admin only
|
|
314
|
+
*
|
|
315
|
+
* Body: { code: "DUBS-XXXX-XXXX" }
|
|
316
|
+
* Response: { success: boolean, code?: string, error?: string }
|
|
317
|
+
*/
|
|
318
|
+
router.post('/release', authenticate, requireAdmin, async (req, res) => {
|
|
319
|
+
try {
|
|
320
|
+
const { code } = req.body;
|
|
321
|
+
|
|
322
|
+
if (!code) {
|
|
323
|
+
return res.status(400).json({ success: false, error: 'Code is required' });
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const result = await promoService.releaseCode(code);
|
|
327
|
+
res.json(result);
|
|
328
|
+
|
|
329
|
+
} catch (error) {
|
|
330
|
+
console.error('[PromoRoutes] Error releasing code:', error.message);
|
|
331
|
+
res.status(500).json({ success: false, error: 'Server error releasing code' });
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* POST /promo/confirm-usage
|
|
337
|
+
* Confirm that a promo code was used (called AFTER transaction is confirmed on-chain)
|
|
338
|
+
* This is the only way to mark a promo as 'used' - prevents marking as used if user cancels
|
|
339
|
+
*
|
|
340
|
+
* Body: { code: "DUBS-XXXX-XXXX", walletAddress: "...", gameId: "...", signature: "..." }
|
|
341
|
+
* Response: { success: boolean, error?: string }
|
|
342
|
+
*/
|
|
343
|
+
router.post('/confirm-usage', async (req, res) => {
|
|
344
|
+
try {
|
|
345
|
+
const { code, walletAddress, gameId, signature } = req.body;
|
|
346
|
+
|
|
347
|
+
if (!code) {
|
|
348
|
+
return res.status(400).json({ success: false, error: 'Code is required' });
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (!walletAddress) {
|
|
352
|
+
return res.status(400).json({ success: false, error: 'Wallet address is required' });
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (!gameId) {
|
|
356
|
+
return res.status(400).json({ success: false, error: 'Game ID is required' });
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (!signature) {
|
|
360
|
+
return res.status(400).json({ success: false, error: 'Transaction signature is required - promo can only be marked used after confirmed tx' });
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
console.log('[PromoRoutes] Confirming usage:', { code, walletAddress, gameId, signature: signature.slice(0, 20) + '...' });
|
|
364
|
+
|
|
365
|
+
// Verify the code belongs to this wallet and is reserved
|
|
366
|
+
const activePromo = await promoService.getActivePromoForUser(walletAddress);
|
|
367
|
+
|
|
368
|
+
if (!activePromo) {
|
|
369
|
+
return res.status(400).json({ success: false, error: 'No active promo found for this wallet' });
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (activePromo.code !== code.trim().toUpperCase()) {
|
|
373
|
+
return res.status(400).json({
|
|
374
|
+
success: false,
|
|
375
|
+
error: `Code mismatch. Expected: ${activePromo.code}, Got: ${code}`
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Mark as used
|
|
380
|
+
const result = await promoService.markCodeAsUsed(code, walletAddress, gameId);
|
|
381
|
+
|
|
382
|
+
if (result.success) {
|
|
383
|
+
console.log('[PromoRoutes] ✅ Promo confirmed as used:', code, 'for game:', gameId);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
res.json(result);
|
|
387
|
+
|
|
388
|
+
} catch (error) {
|
|
389
|
+
console.error('[PromoRoutes] Error confirming usage:', error.message);
|
|
390
|
+
res.status(500).json({ success: false, error: 'Server error confirming promo usage' });
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* POST /promo/dismiss-banner
|
|
396
|
+
* Mark the promo banner as dismissed for a user
|
|
397
|
+
*
|
|
398
|
+
* Body: { walletAddress: "..." }
|
|
399
|
+
* Response: { success: boolean }
|
|
400
|
+
*/
|
|
401
|
+
router.post('/dismiss-banner', async (req, res) => {
|
|
402
|
+
try {
|
|
403
|
+
const { walletAddress } = req.body;
|
|
404
|
+
|
|
405
|
+
if (!walletAddress) {
|
|
406
|
+
return res.status(400).json({ success: false, error: 'Wallet address is required' });
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const result = await promoService.dismissBanner(walletAddress);
|
|
410
|
+
res.json(result);
|
|
411
|
+
|
|
412
|
+
} catch (error) {
|
|
413
|
+
console.error('[PromoRoutes] Error dismissing banner:', error.message);
|
|
414
|
+
res.status(500).json({ success: false, error: 'Server error dismissing banner' });
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
module.exports = router;
|