dubs-server 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (304) hide show
  1. package/.claude/settings.local.json +280 -0
  2. package/CLAUDE.md +46 -0
  3. package/CONNECT4_PRODUCTION_DEPLOY.md +155 -0
  4. package/CURRENT_SESSION.md +171 -0
  5. package/CURRENT_SESSION_DRAW.md +516 -0
  6. package/MARCH_MADNESS_SURVIVOR.md +254 -0
  7. package/PANDA.md +166 -0
  8. package/Procfile +4 -0
  9. package/README.md +476 -0
  10. package/controllers/livescoresController.js +376 -0
  11. package/controllers/pickemController.js +554 -0
  12. package/controllers/survivorAdminController.js +887 -0
  13. package/controllers/survivorController.js +623 -0
  14. package/cron/oracleMonitor.js +77 -0
  15. package/cron/pickemOracleMonitor.js +73 -0
  16. package/data/jackpot-history.json +952 -0
  17. package/data/ncaaTeams.js +406 -0
  18. package/documentation/API_SECURITY_GUIDE.md +327 -0
  19. package/documentation/ARCADE_API.md +593 -0
  20. package/documentation/ARCADE_IMPLEMENTATION_SUMMARY.md +399 -0
  21. package/documentation/ARCADE_QUICKSTART.md +242 -0
  22. package/documentation/AUTOMATIC_MODE_ORACLE.md +321 -0
  23. package/documentation/BUG_FIX_COHORT_DATE_DISPLAY.md +171 -0
  24. package/documentation/CLAIM_MIGRATION_INSTRUCTIONS.md +52 -0
  25. package/documentation/CLAIM_STATUS_FIX.md +67 -0
  26. package/documentation/CLI_TOOL_GUIDE.md +372 -0
  27. package/documentation/COHORT_RETENTION_ANALYSIS.md +295 -0
  28. package/documentation/COHORT_RETENTION_IMPLEMENTATION_COMPLETE.md +461 -0
  29. package/documentation/COHORT_RETENTION_SUMMARY.md +204 -0
  30. package/documentation/COMPLETE_PROJECT_SUMMARY.md +490 -0
  31. package/documentation/DATABASE_QUERIES.md +269 -0
  32. package/documentation/DATABASE_RETENTION_POLICY.md +390 -0
  33. package/documentation/DATABASE_SETUP_GUIDE.md +361 -0
  34. package/documentation/DATABASE_SETUP_SUMMARY.md +247 -0
  35. package/documentation/DEMO_API_CURL_COMMANDS.md +656 -0
  36. package/documentation/DEPLOYMENT_SUMMARY.txt +100 -0
  37. package/documentation/DUPLICATE_NOTIFICATIONS_FIXED.md +201 -0
  38. package/documentation/EXCHANGE_RATES_INTEGRATION.md +371 -0
  39. package/documentation/FINAL_API_PROTECTION_TABLE.md +175 -0
  40. package/documentation/GAME_START_NOTIFICATIONS_DEPLOYMENT.md +256 -0
  41. package/documentation/GAME_START_NOTIFICATIONS_INTEGRATION.md +275 -0
  42. package/documentation/HEROKU_DEPLOYMENT.md +134 -0
  43. package/documentation/HEROKU_SCHEDULER_SETUP.md +271 -0
  44. package/documentation/JACKPOT_API.md +521 -0
  45. package/documentation/JACKPOT_DEPLOYMENT_GUIDE.md +362 -0
  46. package/documentation/JWT_IMPLEMENTATION_SUMMARY.md +373 -0
  47. package/documentation/JWT_QUICK_SETUP.md +268 -0
  48. package/documentation/JWT_TESTING_GUIDE.md +404 -0
  49. package/documentation/KEEPER_RECOVERY_GUIDE.md +381 -0
  50. package/documentation/KEEPER_SETUP.md +206 -0
  51. package/documentation/KEEPER_STATE_MACHINE.md +423 -0
  52. package/documentation/LATEST_PRODUCTION_SETUP.md +387 -0
  53. package/documentation/LOCAL_VOTING_TEST.md +279 -0
  54. package/documentation/ORACLE_FIXES_SUMMARY.md +188 -0
  55. package/documentation/ORACLE_POSTGRESQL_UPDATE.md +202 -0
  56. package/documentation/PAYMENT_DEPLOYMENT.md +209 -0
  57. package/documentation/PNL_TRACKING_SETUP.md +189 -0
  58. package/documentation/PREVENTING_LOCKUP_ERRORS.md +472 -0
  59. package/documentation/PRODUCTION_READY_SUMMARY.md +227 -0
  60. package/documentation/PUBLIC_VS_PRIVATE_ENDPOINTS.md +278 -0
  61. package/documentation/QUICK_AUTH_SETUP.md +99 -0
  62. package/documentation/QUICK_DEPLOY.md +224 -0
  63. package/documentation/QUICK_FIX.md +114 -0
  64. package/documentation/QUICK_START.md +152 -0
  65. package/documentation/REFEREE_MODE_GUIDE.md +392 -0
  66. package/documentation/RETENTION_CORE_ACTION_UPDATE.md +313 -0
  67. package/documentation/RETENTION_UPDATE_SUMMARY.md +108 -0
  68. package/documentation/RUN_MIGRATION_NOW.md +39 -0
  69. package/documentation/SCRIPTS_UPDATE_SUMMARY.md +251 -0
  70. package/documentation/SETUP_GUIDE.md +184 -0
  71. package/documentation/STATE_MACHINE_IMPLEMENTATION.md +250 -0
  72. package/documentation/TELEGRAM_NOTIFICATIONS_DIAGNOSIS.md +361 -0
  73. package/documentation/UNIFIED_ARCHITECTURE.md +231 -0
  74. package/documentation/VOTING_DEPLOYMENT_SUMMARY.md +392 -0
  75. package/documentation/WEBSOCKET_ARCHITECTURE.md +881 -0
  76. package/documentation/WHAT_WE_BUILT_TODAY.md +369 -0
  77. package/documentation/latest/LATEST_PRODUCTION_SETUP.md +865 -0
  78. package/ecosystem.config.js +65 -0
  79. package/env.template +125 -0
  80. package/middleware/apiKeyAuth.js +136 -0
  81. package/middleware/authenticate.js +214 -0
  82. package/middleware/developerUserAuth.js +76 -0
  83. package/middleware/socketAuth.js +69 -0
  84. package/package.json +49 -0
  85. package/postman/Dubs-API-v1-With-Voting.postman_collection.json +555 -0
  86. package/postman/Dubs-API-v1.postman_collection.json +205 -0
  87. package/postman/Dubs_Developer_API.postman_collection.json +662 -0
  88. package/postman/QUICKSTART.md +118 -0
  89. package/postman/QUICK_REFERENCE.md +246 -0
  90. package/postman/README.md +71 -0
  91. package/postman/VOTING_API_GUIDE.md +426 -0
  92. package/refactor/Animations.md +148 -0
  93. package/refactor/Chat.md +252 -0
  94. package/routes/actionsRoutes.js +699 -0
  95. package/routes/adminRoutes.js +370 -0
  96. package/routes/analyticsRoutes.js +1262 -0
  97. package/routes/arcadeRoutes.js +557 -0
  98. package/routes/authRoutes.js +2310 -0
  99. package/routes/avatarRoutes.js +85 -0
  100. package/routes/botRoutes.js +211 -0
  101. package/routes/chatRoutes.js +377 -0
  102. package/routes/cryptoPriceRoutes.js +105 -0
  103. package/routes/developerRoutes.js +4201 -0
  104. package/routes/deviceRoutes.js +214 -0
  105. package/routes/dmRoutes.js +167 -0
  106. package/routes/esportsRoutes.js +806 -0
  107. package/routes/exchangeRateRoutes.js +233 -0
  108. package/routes/gamesRoutes.js +3028 -0
  109. package/routes/jackpotRoutes.js +754 -0
  110. package/routes/keeperMonitoringRoutes.js +156 -0
  111. package/routes/keeperWebhookRoutes.js +466 -0
  112. package/routes/livescoresRoutes.js +31 -0
  113. package/routes/pickemAdminRoutes.js +199 -0
  114. package/routes/pickemRoutes.js +231 -0
  115. package/routes/playerStatsRoutes.js +147 -0
  116. package/routes/portfolioRoutes.js +217 -0
  117. package/routes/promoRoutes.js +418 -0
  118. package/routes/referralEarningsRoutes.js +392 -0
  119. package/routes/socialRoutes.js +459 -0
  120. package/routes/sportsRoutes.js +1271 -0
  121. package/routes/survivorAdminRoutes.js +345 -0
  122. package/routes/survivorRoutes.js +756 -0
  123. package/routes/uploadRoutes.js +256 -0
  124. package/routes/userProfileRoutes.js +244 -0
  125. package/routes/whatsNewRoutes.js +331 -0
  126. package/scripts/.claude/settings.local.json +15 -0
  127. package/scripts/README.md +170 -0
  128. package/scripts/RESTART_EVERYTHING.sh +104 -0
  129. package/scripts/add-claim-columns.sql +48 -0
  130. package/scripts/add-crypto-prices-cache.sql +27 -0
  131. package/scripts/add-exchange-rates-cache.sql +40 -0
  132. package/scripts/add-game-invite-column.sql +23 -0
  133. package/scripts/add-game-invite-notification.sql +33 -0
  134. package/scripts/add-game-invite-telegram-pref.sql +16 -0
  135. package/scripts/add-game-joined-notification.sql +16 -0
  136. package/scripts/add-game-joined-pref.js +40 -0
  137. package/scripts/add-game-joined-preference.sql +6 -0
  138. package/scripts/add-game-start-notifications.sql +41 -0
  139. package/scripts/add-notification-flags-to-games.sql +55 -0
  140. package/scripts/add-pending-game-dismissals.sql +19 -0
  141. package/scripts/add-preferred-currency.sql +34 -0
  142. package/scripts/add-winner-columns.js +61 -0
  143. package/scripts/add_mention_system.sql +53 -0
  144. package/scripts/add_payment_system.sql +96 -0
  145. package/scripts/add_sports_event_id_column.sql +22 -0
  146. package/scripts/analyze-cohort-data-heroku.js +276 -0
  147. package/scripts/analyze-cohort-data.js +295 -0
  148. package/scripts/analyze-prod-cohorts.sh +10 -0
  149. package/scripts/backfill-matchup-images.js +245 -0
  150. package/scripts/backfill-missing-signatures.js +175 -0
  151. package/scripts/backfill-referral-earnings.js +202 -0
  152. package/scripts/check-chat-schema.js +130 -0
  153. package/scripts/check-db.sh +14 -0
  154. package/scripts/check_oracle_in_game.js +54 -0
  155. package/scripts/cleanup-database.js +193 -0
  156. package/scripts/clear-notification-cache.js +85 -0
  157. package/scripts/convert-mnemonic.js +50 -0
  158. package/scripts/create-users-table.sql +44 -0
  159. package/scripts/debug-cohort-counts.js +248 -0
  160. package/scripts/debug-winner-calc.js +84 -0
  161. package/scripts/deploy-payment-system.sh +118 -0
  162. package/scripts/deploy-to-heroku.sh +63 -0
  163. package/scripts/diagnose-locked-round.js +143 -0
  164. package/scripts/dubs-cli.js +720 -0
  165. package/scripts/dump-account.js +65 -0
  166. package/scripts/find-vrf-offset.js +48 -0
  167. package/scripts/fix-chat-notifications-constraint.sql +122 -0
  168. package/scripts/fix-claim-columns.js +124 -0
  169. package/scripts/fix-constraint-now.js +44 -0
  170. package/scripts/fix-lock-timestamps.js +96 -0
  171. package/scripts/fix-locked-round.sh +126 -0
  172. package/scripts/fix-missing-badges.sql +91 -0
  173. package/scripts/fix-payment-notifications.sql +41 -0
  174. package/scripts/force-new-round.js +55 -0
  175. package/scripts/force-resolve-and-claim.js +278 -0
  176. package/scripts/important/README.md +115 -0
  177. package/scripts/important/authority-force-lock.js +197 -0
  178. package/scripts/important/authority-resolve-game.js +267 -0
  179. package/scripts/important/check-game-status.js +373 -0
  180. package/scripts/important/list-pending-games-by-version.js +270 -0
  181. package/scripts/important/reconcile-v1-v2-payouts.js +270 -0
  182. package/scripts/initialize-jackpot.js +111 -0
  183. package/scripts/jackpot/.claude/settings.local.json +10 -0
  184. package/scripts/jackpot/force-reset.js +84 -0
  185. package/scripts/jackpot/initialize-mainnet.js +100 -0
  186. package/scripts/jackpot/keeper.js +742 -0
  187. package/scripts/jackpot/status.js +107 -0
  188. package/scripts/jackpot/update-round-duration.js +143 -0
  189. package/scripts/keeper-bot.js +112 -0
  190. package/scripts/list-pending-games.js +131 -0
  191. package/scripts/migrate-chat-v2.js +127 -0
  192. package/scripts/migrate-chat-winners.js +84 -0
  193. package/scripts/migrate-chat.sh +17 -0
  194. package/scripts/migrate-game-invite.js +83 -0
  195. package/scripts/migrate-heroku-game-notifications.sh +159 -0
  196. package/scripts/migrations/001_analytics_tables.sql +422 -0
  197. package/scripts/migrations/002_add_matchup_image_url.sql +14 -0
  198. package/scripts/migrations/003_referral_earnings.sql +208 -0
  199. package/scripts/migrations/004_add_whats_new_notification_type.sql +62 -0
  200. package/scripts/migrations/005_add_connect4_your_turn_notification.sql +61 -0
  201. package/scripts/migrations/005_push_notifications.sql +55 -0
  202. package/scripts/migrations/006_add_draw_team_players.sql +28 -0
  203. package/scripts/migrations/006_add_game_cancelled_notification.sql +62 -0
  204. package/scripts/migrations/007_add_gif_url.sql +8 -0
  205. package/scripts/migrations/008_add_connect4_columns.sql +139 -0
  206. package/scripts/migrations/008_add_pool_tracking.sql +22 -0
  207. package/scripts/migrations/009_create_survivor_pool_tables.sql +174 -0
  208. package/scripts/migrations/010_add_survivor_pool_outcome.sql +28 -0
  209. package/scripts/migrations/011_create_developer_tables.sql +67 -0
  210. package/scripts/migrations/011_fix_keeper_tables.sql +85 -0
  211. package/scripts/migrations/012_create_developer_webhooks.sql +31 -0
  212. package/scripts/migrations/013_add_network_mode.sql +18 -0
  213. package/scripts/migrations/014_create_developer_app_users.sql +19 -0
  214. package/scripts/migrations/015_add_ui_config.sql +4 -0
  215. package/scripts/migrations/016_add_resolution_secret.sql +4 -0
  216. package/scripts/migrations/017_add_external_game_id.sql +3 -0
  217. package/scripts/migrations/018_create_pickem_tables.sql +115 -0
  218. package/scripts/migrations/019_expo_push_tokens.sql +19 -0
  219. package/scripts/migrations/create_whats_new_tables.sql +88 -0
  220. package/scripts/migrations/drop_live_games_tables.sql +34 -0
  221. package/scripts/open-jackpot-round.js +85 -0
  222. package/scripts/purge-all-data.sh +329 -0
  223. package/scripts/purge-all-data.sql +142 -0
  224. package/scripts/purge-heroku-data.sh +149 -0
  225. package/scripts/purge-heroku-data.sql +62 -0
  226. package/scripts/rebuild-heroku-database.sh +113 -0
  227. package/scripts/recover-funds.js +357 -0
  228. package/scripts/regenerate-epl-images.js +278 -0
  229. package/scripts/resize-s3-matchup-images.js +374 -0
  230. package/scripts/resolve-direct.js +88 -0
  231. package/scripts/resolve-mock-game.js +124 -0
  232. package/scripts/resolve-pickem-game.js +55 -0
  233. package/scripts/resolve-round-manual.js +83 -0
  234. package/scripts/resolve-stuck-game.js +382 -0
  235. package/scripts/resolve-stuck-round.js +42 -0
  236. package/scripts/run-connect4-migration.sh +16 -0
  237. package/scripts/run-mention-migration.sh +32 -0
  238. package/scripts/run-payment-migration.sh +51 -0
  239. package/scripts/run-preferred-currency-migration.sh +31 -0
  240. package/scripts/run-referral-earnings-migration.sh +32 -0
  241. package/scripts/run-survivor-outcome-migration.sh +16 -0
  242. package/scripts/seed-test-users.js +346 -0
  243. package/scripts/setup-auth-tables.js +78 -0
  244. package/scripts/setup-complete-database.sql +992 -0
  245. package/scripts/setup-database-fresh.sh +359 -0
  246. package/scripts/setup-heroku-keeper.sh +48 -0
  247. package/scripts/setup-keeper-database.js +83 -0
  248. package/scripts/setup-keeper-state-db.sql +110 -0
  249. package/scripts/setup-oracle.sh +39 -0
  250. package/scripts/setup-pnl-tracking.js +111 -0
  251. package/scripts/start-devnet.sh +14 -0
  252. package/scripts/test-arcade-devnet.sh +160 -0
  253. package/scripts/test-arcade-match.sh +109 -0
  254. package/scripts/test-automatic-mode.sh +239 -0
  255. package/scripts/test-connect4-cancel-claim.js +370 -0
  256. package/scripts/test-connect4-e2e.js +369 -0
  257. package/scripts/test-connect4-resolve.js +369 -0
  258. package/scripts/test-game-state-endpoint.js +136 -0
  259. package/scripts/test-invite-notification.js +86 -0
  260. package/scripts/test-jackpot-api.sh +71 -0
  261. package/scripts/test-poll-confirmation.js +267 -0
  262. package/scripts/test-resolve-game.js +271 -0
  263. package/scripts/test-resolve-signature.js +223 -0
  264. package/scripts/test-signature-preservation.js +124 -0
  265. package/scripts/test-state-machine.js +291 -0
  266. package/scripts/test-webhook-receiver.js +60 -0
  267. package/scripts/update-notification-constraint.js +52 -0
  268. package/scripts/verify-account-layout.js +145 -0
  269. package/scripts/verify-winner-algorithm.js +278 -0
  270. package/server.js +5259 -0
  271. package/services/arcadeMatchService.js +763 -0
  272. package/services/automaticGameOracle.js +1596 -0
  273. package/services/chatService.js +1612 -0
  274. package/services/connect4GameService.js +1049 -0
  275. package/services/connect4NotificationService.js +374 -0
  276. package/services/cryptoPriceService.js +223 -0
  277. package/services/customGameResolver.js +260 -0
  278. package/services/db.js +79 -0
  279. package/services/directMessageService.js +389 -0
  280. package/services/discordNotifications.js +160 -0
  281. package/services/exchangeRateService.js +289 -0
  282. package/services/expoPushService.js +314 -0
  283. package/services/gamesCacheService.js +539 -0
  284. package/services/jackpotHistory.js +331 -0
  285. package/services/jackpotService.js +856 -0
  286. package/services/keeperStateService.js +355 -0
  287. package/services/matchupImageService.js +591 -0
  288. package/services/notificationCacheService.js +407 -0
  289. package/services/pickemOracle.js +440 -0
  290. package/services/playerStatsService.js +389 -0
  291. package/services/portfolioService.js +555 -0
  292. package/services/promoService.js +757 -0
  293. package/services/promoTreasuryService.js +239 -0
  294. package/services/pushNotifications.js +353 -0
  295. package/services/redisService.js +422 -0
  296. package/services/referralEarningsService.js +728 -0
  297. package/services/s3Service.js +396 -0
  298. package/services/socialService.js +1202 -0
  299. package/services/survivorOracle.js +469 -0
  300. package/services/survivorSimulator.js +475 -0
  301. package/services/telegramNotifications.js +461 -0
  302. package/services/userProfileStatsService.js +1185 -0
  303. package/services/whatsNewService.js +388 -0
  304. package/utils/urlHelper.js +95 -0
@@ -0,0 +1,223 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Test script to verify resolveSignature is being saved correctly
4
+ *
5
+ * Tests:
6
+ * 1. games.claim_signature is saved
7
+ * 2. user_game_refs.claim_signature is saved for winners
8
+ *
9
+ * Usage:
10
+ * node scripts/test-resolve-signature.js
11
+ *
12
+ * Environment:
13
+ * DATABASE_URL - PostgreSQL connection string
14
+ */
15
+
16
+ require('dotenv').config();
17
+ const { Pool } = require('pg');
18
+
19
+ const pool = new Pool({
20
+ connectionString: process.env.DATABASE_URL,
21
+ ssl: process.env.DATABASE_URL?.includes('amazonaws.com') ? { rejectUnauthorized: false } : false
22
+ });
23
+
24
+ const TEST_SIGNATURE = 'TEST_SIG_' + Date.now() + '_ABCDEFGHIJKLMNOP';
25
+
26
+ async function findTestableGame() {
27
+ // Find an unresolved game with players on both sides
28
+ const result = await pool.query(`
29
+ SELECT
30
+ g.game_id,
31
+ g.title,
32
+ g.home_team_players,
33
+ g.away_team_players,
34
+ g.is_resolved,
35
+ g.claim_signature
36
+ FROM games g
37
+ WHERE g.is_resolved = false
38
+ AND array_length(g.home_team_players, 1) > 0
39
+ AND array_length(g.away_team_players, 1) > 0
40
+ ORDER BY g.created_at DESC
41
+ LIMIT 1
42
+ `);
43
+
44
+ if (result.rows.length === 0) {
45
+ return null;
46
+ }
47
+
48
+ return result.rows[0];
49
+ }
50
+
51
+ async function testResolveEndpoint(gameId) {
52
+ const axios = require('axios');
53
+
54
+ const baseUrl = process.env.DUBS_SERVER_URL || 'http://localhost:3001';
55
+ const url = `${baseUrl}/api/games/${gameId}/resolve`;
56
+
57
+ console.log(`\n📡 POST ${url}`);
58
+ console.log(` Body: { winner: 'home', homeScore: 99, awayScore: 98, resolveSignature: '${TEST_SIGNATURE.slice(0, 30)}...' }`);
59
+
60
+ const response = await axios.post(url, {
61
+ winner: 'home',
62
+ homeScore: 99,
63
+ awayScore: 98,
64
+ resolvedAt: new Date().toISOString(),
65
+ resolvedBy: 'test-script',
66
+ resolveSignature: TEST_SIGNATURE
67
+ });
68
+
69
+ return response.data;
70
+ }
71
+
72
+ async function verifyGameSignature(gameId) {
73
+ const result = await pool.query(`
74
+ SELECT game_id, claim_signature, is_resolved
75
+ FROM games
76
+ WHERE game_id = $1
77
+ `, [gameId]);
78
+
79
+ if (result.rows.length === 0) {
80
+ return { success: false, error: 'Game not found' };
81
+ }
82
+
83
+ const game = result.rows[0];
84
+ return {
85
+ success: game.claim_signature === TEST_SIGNATURE,
86
+ claim_signature: game.claim_signature,
87
+ is_resolved: game.is_resolved,
88
+ expected: TEST_SIGNATURE
89
+ };
90
+ }
91
+
92
+ async function verifyWinnersSignature(gameId) {
93
+ const result = await pool.query(`
94
+ SELECT wallet_address, claim_signature
95
+ FROM user_game_refs
96
+ WHERE game_id = $1
97
+ AND claim_signature IS NOT NULL
98
+ `, [gameId]);
99
+
100
+ return {
101
+ winnersUpdated: result.rows.length,
102
+ winners: result.rows.map(r => ({
103
+ wallet: r.wallet_address.slice(0, 8) + '...',
104
+ signature: r.claim_signature?.slice(0, 30) + '...'
105
+ }))
106
+ };
107
+ }
108
+
109
+ async function revertTestGame(gameId, originalState) {
110
+ console.log('\n🔄 Reverting game to original state...');
111
+
112
+ await pool.query(`
113
+ UPDATE games
114
+ SET
115
+ is_resolved = $2,
116
+ claim_signature = $3,
117
+ automatic_status = 'pending',
118
+ sports_event = sports_event - 'finalScore'
119
+ WHERE game_id = $1
120
+ `, [gameId, originalState.is_resolved, originalState.claim_signature]);
121
+
122
+ await pool.query(`
123
+ UPDATE user_game_refs
124
+ SET claim_signature = NULL
125
+ WHERE game_id = $1 AND claim_signature = $2
126
+ `, [gameId, TEST_SIGNATURE]);
127
+
128
+ console.log(' ✅ Reverted');
129
+ }
130
+
131
+ async function main() {
132
+ console.log('═══════════════════════════════════════════════════════════');
133
+ console.log(' Test: resolveSignature saving to database');
134
+ console.log('═══════════════════════════════════════════════════════════');
135
+ console.log(`\nTest signature: ${TEST_SIGNATURE}`);
136
+
137
+ try {
138
+ // Step 1: Find a testable game
139
+ console.log('\n📋 Step 1: Finding an unresolved game with players...');
140
+ const game = await findTestableGame();
141
+
142
+ if (!game) {
143
+ console.log(' ❌ No testable game found (need unresolved game with home and away players)');
144
+ console.log(' You may need to create a test game first.');
145
+ process.exit(1);
146
+ }
147
+
148
+ console.log(` ✅ Found: ${game.game_id}`);
149
+ console.log(` Title: ${game.title}`);
150
+ console.log(` Home players: ${game.home_team_players?.length || 0}`);
151
+ console.log(` Away players: ${game.away_team_players?.length || 0}`);
152
+ console.log(` Current claim_signature: ${game.claim_signature || 'NULL'}`);
153
+
154
+ const originalState = {
155
+ is_resolved: game.is_resolved,
156
+ claim_signature: game.claim_signature
157
+ };
158
+
159
+ // Step 2: Call resolve endpoint
160
+ console.log('\n📋 Step 2: Calling resolve endpoint with test signature...');
161
+ try {
162
+ const resolveResult = await testResolveEndpoint(game.game_id);
163
+ console.log(` ✅ Response: ${resolveResult.success ? 'success' : 'failed'}`);
164
+ } catch (err) {
165
+ console.log(` ❌ Request failed: ${err.message}`);
166
+ if (err.response) {
167
+ console.log(` Status: ${err.response.status}`);
168
+ console.log(` Data: ${JSON.stringify(err.response.data)}`);
169
+ }
170
+ process.exit(1);
171
+ }
172
+
173
+ // Step 3: Verify games.claim_signature
174
+ console.log('\n📋 Step 3: Verifying games.claim_signature...');
175
+ const gameVerify = await verifyGameSignature(game.game_id);
176
+
177
+ if (gameVerify.success) {
178
+ console.log(` ✅ PASS: games.claim_signature saved correctly`);
179
+ console.log(` Saved: ${gameVerify.claim_signature?.slice(0, 40)}...`);
180
+ } else {
181
+ console.log(` ❌ FAIL: games.claim_signature NOT saved correctly`);
182
+ console.log(` Expected: ${gameVerify.expected?.slice(0, 40)}...`);
183
+ console.log(` Got: ${gameVerify.claim_signature || 'NULL'}`);
184
+ }
185
+
186
+ // Step 4: Verify user_game_refs.claim_signature for winners
187
+ console.log('\n📋 Step 4: Verifying user_game_refs.claim_signature for winners...');
188
+ const winnersVerify = await verifyWinnersSignature(game.game_id);
189
+
190
+ if (winnersVerify.winnersUpdated > 0) {
191
+ console.log(` ✅ PASS: ${winnersVerify.winnersUpdated} winner(s) updated with claim_signature`);
192
+ winnersVerify.winners.forEach(w => {
193
+ console.log(` - ${w.wallet}: ${w.signature}`);
194
+ });
195
+ } else {
196
+ console.log(` ⚠️ WARNING: No winners updated with claim_signature`);
197
+ console.log(` This could be a bug, or there were no home players (we set winner='home')`);
198
+ }
199
+
200
+ // Step 5: Revert
201
+ console.log('\n📋 Step 5: Reverting test changes...');
202
+ await revertTestGame(game.game_id, originalState);
203
+
204
+ // Summary
205
+ console.log('\n═══════════════════════════════════════════════════════════');
206
+ console.log(' TEST RESULTS');
207
+ console.log('═══════════════════════════════════════════════════════════');
208
+ console.log(` games.claim_signature: ${gameVerify.success ? '✅ PASS' : '❌ FAIL'}`);
209
+ console.log(` user_game_refs signatures: ${winnersVerify.winnersUpdated > 0 ? '✅ PASS' : '⚠️ CHECK'}`);
210
+ console.log('═══════════════════════════════════════════════════════════\n');
211
+
212
+ process.exit(gameVerify.success ? 0 : 1);
213
+
214
+ } catch (error) {
215
+ console.error('\n❌ Test failed with error:', error.message);
216
+ console.error(error.stack);
217
+ process.exit(1);
218
+ } finally {
219
+ await pool.end();
220
+ }
221
+ }
222
+
223
+ main();
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Test script to verify that my_signature is preserved on upsert
4
+ *
5
+ * Tests the fix for: ON CONFLICT should use COALESCE to preserve existing signatures
6
+ *
7
+ * Run with: node scripts/test-signature-preservation.js
8
+ */
9
+
10
+ const { Pool } = require('pg');
11
+ require('dotenv').config();
12
+
13
+ const pool = new Pool({
14
+ connectionString: process.env.DATABASE_URL,
15
+ });
16
+
17
+ async function runTest() {
18
+ const testWallet = 'TEST_WALLET_' + Date.now();
19
+ const testGameId = 'c4-test-' + Date.now();
20
+ const testSignature = 'TEST_SIGNATURE_' + Date.now();
21
+ const testExplorerUrl = 'https://explorer.solana.com/tx/' + testSignature;
22
+
23
+ console.log('🧪 Testing signature preservation fix...\n');
24
+
25
+ try {
26
+ // Step 1: Insert a record WITH signature
27
+ console.log('1️⃣ Inserting record with signature...');
28
+ await pool.query(`
29
+ INSERT INTO user_game_refs (
30
+ wallet_address, game_id, role, joined_at, team_choice,
31
+ my_signature, my_explorer_url, status
32
+ ) VALUES ($1, $2, $3, NOW(), $4, $5, $6, $7)
33
+ `, [testWallet, testGameId, 'player', 'away', testSignature, testExplorerUrl, 'active']);
34
+
35
+ // Verify it was inserted correctly
36
+ const afterInsert = await pool.query(
37
+ 'SELECT my_signature, my_explorer_url FROM user_game_refs WHERE wallet_address = $1 AND game_id = $2',
38
+ [testWallet, testGameId]
39
+ );
40
+ console.log(' Inserted signature:', afterInsert.rows[0].my_signature);
41
+ console.log(' ✅ Signature saved correctly\n');
42
+
43
+ // Step 2: Simulate an upsert WITHOUT signature (like error 6030 handler)
44
+ console.log('2️⃣ Upserting WITHOUT signature (simulating error 6030 handler)...');
45
+ await pool.query(`
46
+ INSERT INTO user_game_refs (
47
+ wallet_address, game_id, role, joined_at, team_choice,
48
+ my_signature, my_explorer_url, status, wallet_type
49
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
50
+ ON CONFLICT (wallet_address, game_id)
51
+ DO UPDATE SET
52
+ team_choice = EXCLUDED.team_choice,
53
+ my_signature = COALESCE(EXCLUDED.my_signature, user_game_refs.my_signature),
54
+ my_explorer_url = COALESCE(EXCLUDED.my_explorer_url, user_game_refs.my_explorer_url),
55
+ status = EXCLUDED.status
56
+ `, [testWallet, testGameId, 'player', new Date().toISOString(), 'away', null, null, 'updated', null]);
57
+
58
+ // Verify signature was preserved
59
+ const afterUpsert = await pool.query(
60
+ 'SELECT my_signature, my_explorer_url, status FROM user_game_refs WHERE wallet_address = $1 AND game_id = $2',
61
+ [testWallet, testGameId]
62
+ );
63
+
64
+ console.log(' After upsert signature:', afterUpsert.rows[0].my_signature);
65
+ console.log(' After upsert status:', afterUpsert.rows[0].status);
66
+
67
+ if (afterUpsert.rows[0].my_signature === testSignature) {
68
+ console.log(' ✅ PASS: Signature was preserved!\n');
69
+ } else {
70
+ console.log(' ❌ FAIL: Signature was overwritten with:', afterUpsert.rows[0].my_signature, '\n');
71
+ process.exit(1);
72
+ }
73
+
74
+ // Step 3: Verify that a NEW signature CAN overwrite (should still work)
75
+ console.log('3️⃣ Upserting WITH new signature (should update)...');
76
+ const newSignature = 'NEW_SIGNATURE_' + Date.now();
77
+ await pool.query(`
78
+ INSERT INTO user_game_refs (
79
+ wallet_address, game_id, role, joined_at, team_choice,
80
+ my_signature, my_explorer_url, status, wallet_type
81
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
82
+ ON CONFLICT (wallet_address, game_id)
83
+ DO UPDATE SET
84
+ team_choice = EXCLUDED.team_choice,
85
+ my_signature = COALESCE(EXCLUDED.my_signature, user_game_refs.my_signature),
86
+ my_explorer_url = COALESCE(EXCLUDED.my_explorer_url, user_game_refs.my_explorer_url),
87
+ status = EXCLUDED.status
88
+ `, [testWallet, testGameId, 'player', new Date().toISOString(), 'away', newSignature, 'new-url', 'active', null]);
89
+
90
+ const afterNewSig = await pool.query(
91
+ 'SELECT my_signature FROM user_game_refs WHERE wallet_address = $1 AND game_id = $2',
92
+ [testWallet, testGameId]
93
+ );
94
+
95
+ if (afterNewSig.rows[0].my_signature === newSignature) {
96
+ console.log(' ✅ PASS: New signature was correctly saved!\n');
97
+ } else {
98
+ console.log(' ❌ FAIL: New signature was not saved\n');
99
+ process.exit(1);
100
+ }
101
+
102
+ // Cleanup
103
+ console.log('🧹 Cleaning up test data...');
104
+ await pool.query(
105
+ 'DELETE FROM user_game_refs WHERE wallet_address = $1',
106
+ [testWallet]
107
+ );
108
+
109
+ console.log('\n✅ All tests passed! The COALESCE fix is working correctly.');
110
+
111
+ } catch (error) {
112
+ console.error('❌ Test error:', error);
113
+ // Cleanup on error
114
+ await pool.query(
115
+ 'DELETE FROM user_game_refs WHERE wallet_address LIKE $1',
116
+ ['TEST_WALLET_%']
117
+ );
118
+ process.exit(1);
119
+ } finally {
120
+ await pool.end();
121
+ }
122
+ }
123
+
124
+ runTest();
@@ -0,0 +1,291 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * State Machine Test Suite
5
+ *
6
+ * Tests that the keeper database monitoring actually helps:
7
+ * 1. Recovery from crashes
8
+ * 2. Stuck round detection
9
+ * 3. Retry tracking
10
+ * 4. Health monitoring
11
+ */
12
+
13
+ const KeeperStateService = require('../services/keeperStateService');
14
+
15
+ console.log('🧪 KEEPER STATE MACHINE TEST SUITE\n');
16
+ console.log('='.repeat(80));
17
+
18
+ const state = new KeeperStateService();
19
+
20
+ async function runTests() {
21
+ try {
22
+ // ========================================================================
23
+ // TEST 1: Basic CRUD Operations
24
+ // ========================================================================
25
+ console.log('\n📝 TEST 1: Basic Database Operations');
26
+ console.log('-'.repeat(80));
27
+
28
+ const testRoundId = 999999;
29
+
30
+ // Create round
31
+ console.log('Creating test round 999999...');
32
+ await state.createRound(testRoundId, 'open');
33
+ let round = await state.getRound(testRoundId);
34
+
35
+ if (round && round.status === 'open') {
36
+ console.log('✅ Create: Round created with status "open"');
37
+ } else {
38
+ console.log('❌ Create: Failed');
39
+ return;
40
+ }
41
+
42
+ // Update round
43
+ console.log('Updating to "locking"...');
44
+ await state.updateRound(testRoundId, {
45
+ status: 'locking',
46
+ last_attempt_at: new Date()
47
+ });
48
+ round = await state.getRound(testRoundId);
49
+
50
+ if (round && round.status === 'locking') {
51
+ console.log('✅ Update: Round updated to "locking"');
52
+ } else {
53
+ console.log('❌ Update: Failed');
54
+ return;
55
+ }
56
+
57
+ // Log action
58
+ console.log('Logging action...');
59
+ await state.logAction(testRoundId, 'lock', true, null, 'test-sig-123', 1234);
60
+ const actions = await state.getRoundActions(testRoundId, 10);
61
+
62
+ if (actions.length > 0 && actions[0].action === 'lock') {
63
+ console.log('✅ Log Action: Action logged successfully');
64
+ } else {
65
+ console.log('❌ Log Action: Failed');
66
+ return;
67
+ }
68
+
69
+ console.log('\n✅ TEST 1 PASSED: Basic operations work');
70
+
71
+ // ========================================================================
72
+ // TEST 2: Stuck Round Detection
73
+ // ========================================================================
74
+ console.log('\n\n⏱️ TEST 2: Stuck Round Detection');
75
+ console.log('-'.repeat(80));
76
+
77
+ // Create a round that looks stuck (old timestamp, incomplete status)
78
+ const stuckRoundId = 999998;
79
+ await state.createRound(stuckRoundId, 'locking');
80
+
81
+ // Manually set old timestamp to simulate stuck round (disable trigger)
82
+ await state.pool.query(`ALTER TABLE keeper_rounds DISABLE TRIGGER update_keeper_rounds_updated_at`);
83
+ await state.pool.query(
84
+ `UPDATE keeper_rounds
85
+ SET updated_at = NOW() - INTERVAL '10 minutes',
86
+ last_attempt_at = NOW() - INTERVAL '10 minutes'
87
+ WHERE round_id = $1`,
88
+ [stuckRoundId]
89
+ );
90
+ await state.pool.query(`ALTER TABLE keeper_rounds ENABLE TRIGGER update_keeper_rounds_updated_at`);
91
+
92
+ console.log('Created simulated stuck round (999998 stuck in "locking" for 10 minutes)');
93
+
94
+ // Check if detection works
95
+ const stuckRounds = await state.getStuckRounds();
96
+ const foundStuck = stuckRounds.find(r => r.round_id === stuckRoundId.toString());
97
+
98
+ if (foundStuck) {
99
+ console.log(`✅ Stuck Detection: Round 999998 detected as stuck`);
100
+ console.log(` Status: ${foundStuck.status}`);
101
+ console.log(` Stuck for: ${Math.floor(foundStuck.seconds_stuck)} seconds`);
102
+ } else {
103
+ console.log('❌ Stuck Detection: Failed to detect stuck round');
104
+ return;
105
+ }
106
+
107
+ // Test isRoundStuck helper
108
+ const stuckRound = await state.getRound(stuckRoundId);
109
+ if (state.isRoundStuck(stuckRound)) {
110
+ console.log('✅ isRoundStuck: Helper function works correctly');
111
+ } else {
112
+ console.log('❌ isRoundStuck: Helper failed');
113
+ return;
114
+ }
115
+
116
+ console.log('\n✅ TEST 2 PASSED: Stuck round detection works');
117
+
118
+ // ========================================================================
119
+ // TEST 3: Retry Tracking
120
+ // ========================================================================
121
+ console.log('\n\n🔄 TEST 3: Retry Tracking');
122
+ console.log('-'.repeat(80));
123
+
124
+ const retryRoundId = 999997;
125
+ await state.createRound(retryRoundId, 'revealing');
126
+
127
+ console.log('Simulating 3 failed attempts...');
128
+ for (let i = 1; i <= 3; i++) {
129
+ await state.incrementRetry(retryRoundId, `Test error #${i}`);
130
+ await state.logAction(retryRoundId, 'reveal', false, `Test error #${i}`, null, 500);
131
+ }
132
+
133
+ const retryRound = await state.getRound(retryRoundId);
134
+
135
+ if (retryRound.retry_count === 3 && retryRound.last_error === 'Test error #3') {
136
+ console.log(`✅ Retry Tracking: ${retryRound.retry_count} retries logged`);
137
+ console.log(`✅ Error Tracking: Last error stored: "${retryRound.last_error}"`);
138
+ } else {
139
+ console.log('❌ Retry Tracking: Failed');
140
+ console.log(` Expected 3 retries, got: ${retryRound.retry_count}`);
141
+ return;
142
+ }
143
+
144
+ const retryActions = await state.getRoundActions(retryRoundId);
145
+ const failedActions = retryActions.filter(a => !a.success);
146
+
147
+ if (failedActions.length === 3) {
148
+ console.log(`✅ Action Log: All ${failedActions.length} failures logged`);
149
+ } else {
150
+ console.log('❌ Action Log: Failed');
151
+ return;
152
+ }
153
+
154
+ console.log('\n✅ TEST 3 PASSED: Retry tracking works');
155
+
156
+ // ========================================================================
157
+ // TEST 4: Health Metrics
158
+ // ========================================================================
159
+ console.log('\n\n💊 TEST 4: Health Metrics');
160
+ console.log('-'.repeat(80));
161
+
162
+ console.log('Recording health snapshot...');
163
+ await state.recordHealth({
164
+ roundsCompleted: 42,
165
+ consecutiveFailures: 0,
166
+ lastSuccessRound: 551,
167
+ lastError: null,
168
+ uptimeSeconds: 3600
169
+ });
170
+
171
+ const healthRecords = await state.pool.query(
172
+ 'SELECT * FROM keeper_health ORDER BY timestamp DESC LIMIT 1'
173
+ );
174
+
175
+ if (healthRecords.rows.length > 0) {
176
+ const latest = healthRecords.rows[0];
177
+ console.log(`✅ Health Recording: Snapshot saved`);
178
+ console.log(` Rounds completed: ${latest.rounds_completed}`);
179
+ console.log(` Consecutive failures: ${latest.consecutive_failures}`);
180
+ console.log(` Uptime: ${latest.uptime_seconds}s`);
181
+ } else {
182
+ console.log('❌ Health Recording: Failed');
183
+ return;
184
+ }
185
+
186
+ console.log('\n✅ TEST 4 PASSED: Health metrics work');
187
+
188
+ // ========================================================================
189
+ // TEST 5: Recovery Scenario Simulation
190
+ // ========================================================================
191
+ console.log('\n\n🔧 TEST 5: Recovery Scenario Simulation');
192
+ console.log('-'.repeat(80));
193
+
194
+ const recoveryRoundId = 999996;
195
+
196
+ console.log('Scenario: Keeper crashes after locking round but before reveal');
197
+ console.log(' 1. Create round in "locked" status (simulating successful lock)');
198
+ await state.createRound(recoveryRoundId, 'locked');
199
+ await state.updateRound(recoveryRoundId, {
200
+ locked_at: new Date(),
201
+ lock_signature: 'abc123...'
202
+ });
203
+
204
+ console.log(' 2. Check what recovery logic should do');
205
+ const roundToRecover = await state.getRound(recoveryRoundId);
206
+
207
+ if (roundToRecover.status === 'locked' && roundToRecover.locked_at && !roundToRecover.revealed_at) {
208
+ console.log('✅ Recovery Detection: Identified round needs reveal step');
209
+ console.log(' Next action: Should call revealRandomness()');
210
+ } else {
211
+ console.log('❌ Recovery Detection: Failed');
212
+ return;
213
+ }
214
+
215
+ console.log('\n 3. Simulate successful recovery (complete reveal)');
216
+ await state.updateRound(recoveryRoundId, {
217
+ status: 'revealed',
218
+ revealed_at: new Date(),
219
+ reveal_signature: 'def456...'
220
+ });
221
+ await state.logAction(recoveryRoundId, 'reveal', true, null, 'def456...', 987);
222
+
223
+ const recoveredRound = await state.getRound(recoveryRoundId);
224
+
225
+ if (recoveredRound.status === 'revealed' && recoveredRound.revealed_at) {
226
+ console.log('✅ Recovery Execution: Round successfully recovered and progressed');
227
+ } else {
228
+ console.log('❌ Recovery Execution: Failed');
229
+ return;
230
+ }
231
+
232
+ console.log('\n✅ TEST 5 PASSED: Recovery scenario works');
233
+
234
+ // ========================================================================
235
+ // TEST 6: Health Summary View
236
+ // ========================================================================
237
+ console.log('\n\n📊 TEST 6: Health Summary View');
238
+ console.log('-'.repeat(80));
239
+
240
+ const summary = await state.getHealthSummary();
241
+
242
+ if (summary) {
243
+ console.log('✅ Health Summary View works:');
244
+ console.log(` Resolved rounds: ${summary.resolved_rounds}`);
245
+ console.log(` Stuck rounds: ${summary.stuck_rounds}`);
246
+ console.log(` Rounds with retries: ${summary.rounds_with_retries}`);
247
+ console.log(` Avg completion time: ${summary.avg_completion_seconds || 'N/A'}s`);
248
+ } else {
249
+ console.log('❌ Health Summary: Failed');
250
+ return;
251
+ }
252
+
253
+ console.log('\n✅ TEST 6 PASSED: Health summary view works');
254
+
255
+ // ========================================================================
256
+ // CLEANUP
257
+ // ========================================================================
258
+ console.log('\n\n🧹 Cleaning up test data...');
259
+ await state.pool.query('DELETE FROM keeper_rounds WHERE round_id >= 999996');
260
+ await state.pool.query('DELETE FROM keeper_actions WHERE round_id >= 999996');
261
+ console.log('✅ Test data cleaned up');
262
+
263
+ // ========================================================================
264
+ // FINAL VERDICT
265
+ // ========================================================================
266
+ console.log('\n\n' + '='.repeat(80));
267
+ console.log('🎉 ALL TESTS PASSED!');
268
+ console.log('='.repeat(80));
269
+ console.log(`
270
+ ✅ Database CRUD operations work
271
+ ✅ Stuck round detection works
272
+ ✅ Retry tracking works
273
+ ✅ Health metrics work
274
+ ✅ Recovery scenario simulation works
275
+ ✅ Health summary view works
276
+
277
+ The keeper state machine is PROVEN to work correctly.
278
+ Safe to rely on for production.
279
+ `);
280
+ console.log('='.repeat(80));
281
+
282
+ } catch (error) {
283
+ console.error('\n❌ TEST FAILED:', error.message);
284
+ console.error(error);
285
+ } finally {
286
+ await state.close();
287
+ }
288
+ }
289
+
290
+ runTests();
291
+
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Simple webhook test receiver
3
+ * Run: node scripts/test-webhook-receiver.js
4
+ * Listens on port 3002 and logs all incoming webhook payloads
5
+ */
6
+
7
+ const http = require('http');
8
+ const crypto = require('crypto');
9
+
10
+ const PORT = 3005;
11
+
12
+ // If you have a webhook secret, paste it here to verify signatures
13
+ const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET || '';
14
+
15
+ const server = http.createServer((req, res) => {
16
+ if (req.method !== 'POST') {
17
+ res.writeHead(200);
18
+ res.end('Webhook receiver running. POST to / to test.');
19
+ return;
20
+ }
21
+
22
+ let body = '';
23
+ req.on('data', chunk => { body += chunk; });
24
+ req.on('end', () => {
25
+ const event = req.headers['x-dubs-event'] || '(none)';
26
+ const signature = req.headers['x-dubs-signature'] || '(none)';
27
+
28
+ console.log('\n' + '='.repeat(60));
29
+ console.log(`📥 WEBHOOK RECEIVED — ${new Date().toLocaleTimeString()}`);
30
+ console.log('='.repeat(60));
31
+ console.log(`Event: ${event}`);
32
+ console.log(`Signature: ${signature}`);
33
+
34
+ // Verify signature if secret is set
35
+ if (WEBHOOK_SECRET) {
36
+ const expected = crypto.createHmac('sha256', WEBHOOK_SECRET).update(body).digest('hex');
37
+ const valid = expected === signature;
38
+ console.log(`Verified: ${valid ? '✅ VALID' : '❌ INVALID'}`);
39
+ }
40
+
41
+ try {
42
+ const parsed = JSON.parse(body);
43
+ console.log(`\nPayload:`);
44
+ console.log(JSON.stringify(parsed, null, 2));
45
+ } catch {
46
+ console.log(`\nRaw body: ${body}`);
47
+ }
48
+ console.log('='.repeat(60));
49
+
50
+ res.writeHead(200, { 'Content-Type': 'application/json' });
51
+ res.end(JSON.stringify({ received: true }));
52
+ });
53
+ });
54
+
55
+ server.listen(PORT, () => {
56
+ console.log(`\n🎯 Webhook test receiver listening on http://localhost:${PORT}`);
57
+ console.log(`\nRegister this URL in the Webhooks tab:`);
58
+ console.log(` http://localhost:${PORT}`);
59
+ console.log(`\nThen create or join a game in the playground — you'll see the payload here.\n`);
60
+ });