add address modal and try to fix payout

This commit is contained in:
jwinterm
2025-09-29 17:40:59 -04:00
parent 95711b9d26
commit 4e7a773496
15 changed files with 806 additions and 85 deletions
+29 -4
View File
@@ -22,6 +22,8 @@ class Game {
this.players = {};
this.monsters = {};
this.dungeon = null;
this.moveCount = 0;
this.startedAt = Date.now();
// Get default configuration from DungeonGenerator
const dungeonConfig = DungeonGenerator.getConfig();
@@ -140,7 +142,8 @@ class Game {
this.dungeon.map[newY][newX] !== undefined &&
(this.dungeon.map[newY][newX] === primaryFloor || this.dungeon.map[newY][newX] === secondaryFloor)) {
this.player.moveTo(newX, newY);
this.player.moveTo(newX, newY);
this.moveCount += 1;
if (CONSOLE_LOGGING) {
console.log(`Player moved to ${newX},${newY} in game for socket ${this.socketId}`);
}
@@ -151,7 +154,7 @@ class Game {
// Check for game events like finding treasure or exit
if (this.dungeon.exit && this.player.isAt(this.dungeon.exit[0], this.dungeon.exit[1])) {
this.gameState = 'won';
return { status: 'moved', event: 'escaped', player: this.player.getState(), visibleTiles: this.visibleTiles };
return { status: 'moved', event: 'escaped', player: this.player.getState(), visibleTiles: this.visibleTiles, moves: this.moveCount };
}
if (this.dungeon.treasure && this.player.isAt(this.dungeon.treasure[0], this.dungeon.treasure[1]) && !this.player.hasTreasure) {
this.player.hasTreasure = true;
@@ -163,10 +166,10 @@ class Game {
console.log(`🗑️ TREASURE REMOVED: dungeon.treasure is now null for player ${this.socketId}`);
console.log(`Player ${this.socketId} collected treasure! hasTreasure: ${this.player.hasTreasure}`);
}
return { status: 'moved', event: 'treasure_found', player: this.player.getState(), visibleTiles: this.visibleTiles };
return { status: 'moved', event: 'treasure_found', player: this.player.getState(), visibleTiles: this.visibleTiles, moves: this.moveCount };
}
return { status: 'moved', player: this.player.getState(), visibleTiles: this.visibleTiles };
return { status: 'moved', player: this.player.getState(), visibleTiles: this.visibleTiles, moves: this.moveCount };
}
if (CONSOLE_LOGGING) {
@@ -176,7 +179,27 @@ class Game {
}
moveMonster() {
if (!this.monster || !this.player) {
return { status: 'idle' };
}
this.monster.moveTowardPlayer(this.player, this.dungeon);
if (this.monster.hasCaughtPlayer(this.player)) {
this.gameState = 'lost';
return {
status: 'monster_caught',
event: 'monster_caught',
player: this.player.getState(),
monster: this.monster.getState()
};
}
return {
status: 'moved',
player: this.player.getState(),
monster: this.monster.getState()
};
}
getState() {
@@ -189,6 +212,8 @@ class Game {
entrance: this.dungeon ? this.dungeon.entrance : null,
exit: this.dungeon ? this.dungeon.exit : null,
treasure: this.dungeon ? this.dungeon.treasure : null,
moves: this.moveCount,
startedAt: this.startedAt
};
if (CONSOLE_LOGGING) {
+39 -13
View File
@@ -54,12 +54,17 @@ class GameManager {
async handleGameOver(socket, game, status, reason, message, score = 0) {
try {
const socketId = socket.id || socket;
game.gameState = status;
const gameStats = {
score: score,
const moves = game.moveCount || 0;
const durationSeconds = game.startedAt ? Math.max(0, Math.round((Date.now() - game.startedAt) / 1000)) : null;
const finalScore = score > 0 ? score : this._calculateScore(game, status, reason);
const gameStats = {
score: finalScore,
reason: reason,
treasuresFound: game.player.hasTreasure ? 1 : 0
treasuresFound: game.player.hasTreasure ? 1 : 0,
moves,
durationSeconds
};
game.endGame(status, gameStats);
@@ -71,7 +76,8 @@ class GameManager {
socketId,
game.id,
status === 'won',
game.player.hasTreasure || false
game.player.hasTreasure || false,
{ moves, durationSeconds, score: finalScore }
);
if (this.debugManager.CONSOLE_LOGGING && payoutInfo) {
@@ -87,13 +93,15 @@ class GameManager {
status: status,
reason: reason,
message: message,
score: score,
score: finalScore,
moves,
durationSeconds,
payout: payoutInfo,
treasure: game.player.hasTreasure || false
});
// Persist completion details if DB available
await this._updateGameRecord(game, socketId, status, reason);
await this._updateGameRecord(game, socketId, status, reason, moves, durationSeconds);
// Clean up game from active games
this.activeGames.delete(socketId);
@@ -199,6 +207,24 @@ class GameManager {
// Private helper methods
_calculateScore(game, status, reason) {
if (!game) return 0;
const moves = game.moveCount || 0;
let score = 0;
if (status === 'won' && reason === 'escaped') {
score += 100; // base win
if (game.player?.hasTreasure) {
score += 100; // treasure bonus
}
const moveBonus = Math.max(0, 100 - Math.max(moves - 40, 0) * 2);
score += moveBonus;
} else if (game.player?.hasTreasure) {
// Reached treasure but did not escape
score += 50;
}
return Math.max(0, Math.round(score));
}
/**
* Insert a database record for the game
* @param {Object} game - Game instance
@@ -213,8 +239,8 @@ class GameManager {
db.query(`
INSERT INTO games (user_id, socket_id, game_mode, status, start_block_height, dungeon_seed, created_at)
VALUES ((SELECT id FROM users WHERE socket_id = $1), $1, $2, 'active', $3, $4, NOW())
`, [socketId, gameMode, blockHeight, game.id])
VALUES ((SELECT id FROM users WHERE socket_id = $1), $2, $3, 'active', $4, $5, NOW())
`, [socketId, socketId, gameMode, blockHeight, game.id])
.catch(err => console.error('Game insert failed:', err.message));
}
}
@@ -226,16 +252,16 @@ class GameManager {
* @param {string} status - Final game status
* @param {string} reason - Reason for ending
*/
async _updateGameRecord(game, socketId, status, reason) {
async _updateGameRecord(game, socketId, status, reason, moves = 0, durationSeconds = null) {
if (this.gameModeManager && this.gameModeManager.db) {
const db = this.gameModeManager.db;
const outcome = reason === 'escaped' ? 'escaped' : (reason === 'monster' ? 'caught_by_monster' : reason);
try {
await db.query(`
UPDATE games SET status = $1, outcome = $2, treasure_found = $3, completed_at = NOW()
WHERE dungeon_seed = $4 AND socket_id = $5
`, [status, outcome, game.player.hasTreasure, game.id, socketId]);
UPDATE games SET status = $1, outcome = $2, treasure_found = $3, moves_made = $4, duration_seconds = $5, completed_at = NOW()
WHERE dungeon_seed = $6 AND socket_id = $7
`, [status, outcome, game.player.hasTreasure, moves, durationSeconds, game.id, socketId]);
if (this.debugManager.CONSOLE_LOGGING) {
console.log(`✅ Updated game record for ${socketId}: ${status} (${outcome})`);
+31 -19
View File
@@ -425,12 +425,14 @@ class GameModeManager {
if (payoutEligibleMode && outcome === 'escaped' && game.payout_address) {
const { amount: payoutAmount, multiplier } = this.calculatePayout(recordedMode, { treasureFound });
if (payoutAmount > 0) {
await this.walletService.processPayout(
game.user_id,
await this.walletService.processPayout({
userId: game.user_id,
gameId,
game.payout_address,
payoutAmount
);
address: game.payout_address,
amount: payoutAmount,
multiplier,
description: `Game ${gameId} payout`
});
console.log(`💸 Created payout: ${payoutAmount} atomic units for game ${gameId} (multiplier ${multiplier}x)`);
}
@@ -644,10 +646,10 @@ class GameModeManager {
* @param {boolean} treasureFound - Whether treasure was found
* @returns {object} payout / completion info
*/
async completeGame(socketId, gameId, won, treasureFound) {
async completeGame(socketId, gameId, won, treasureFound, metrics = {}) {
// For FREE mode we just return basic info
if (this.gameMode === 'FREE') {
return { success: true, mode: 'FREE', payout: null };
return { success: true, mode: 'FREE', payout: null, score: metrics.score ?? null };
}
try {
@@ -655,9 +657,16 @@ class GameModeManager {
// If a games table exists with at least id & user reference, mark completion.
try {
await this.db.query(`
UPDATE games SET status = $1, treasure_found = $2, completed_at = NOW()
WHERE id = $3
`, [won ? 'won' : 'lost', treasureFound, gameId]);
UPDATE games SET status = $1, treasure_found = $2, moves_made = COALESCE($3, moves_made), duration_seconds = COALESCE($4, duration_seconds), completed_at = NOW()
WHERE dungeon_seed = $5 AND socket_id = $6
`, [
won ? 'won' : 'lost',
treasureFound,
metrics.moves ?? null,
metrics.durationSeconds ?? null,
gameId,
socketId
]);
} catch (e) {
// Non-fatal if games table differs during early dev.
if (process.env.NODE_ENV === 'development') {
@@ -675,12 +684,14 @@ class GameModeManager {
const userRow = userResult.rows[0];
if (userRow && userRow.payout_address) {
try {
await this.walletService.processPayout(
userRow.id,
await this.walletService.processPayout({
userId: userRow.id,
gameId,
userRow.payout_address,
payoutAmount
);
address: userRow.payout_address,
amount: payoutAmount,
multiplier,
description: `Game ${gameId} payout`
});
return {
success: true,
mode: this.gameMode,
@@ -688,19 +699,20 @@ class GameModeManager {
amount: payoutAmount,
multiplier,
treasure: treasureFound
}
},
score: metrics.score ?? null
};
} catch (payoutErr) {
const normalizedPayout = normalizeError(payoutErr, 'Failed to send payout');
console.error('❌ Error creating payout:', normalizedPayout.message);
return { success: true, mode: this.gameMode, payout: null, payoutError: normalizedPayout.safeMessage };
return { success: true, mode: this.gameMode, payout: null, payoutError: normalizedPayout.safeMessage, score: metrics.score ?? null };
}
}
return { success: true, mode: this.gameMode, payout: null, reason: 'No payout address' };
return { success: true, mode: this.gameMode, payout: null, reason: 'No payout address', score: metrics.score ?? null };
}
// Credits mode: decrement nothing here (already handled start). Optionally could award stats.
return { success: true, mode: this.gameMode, payout: null };
return { success: true, mode: this.gameMode, payout: null, score: metrics.score ?? null };
} catch (err) {
const normalized = normalizeError(err, 'Failed to complete game');
console.error('❌ completeGame error:', normalized.message);
+17
View File
@@ -64,6 +64,23 @@ class MovementManager {
this.io.to(socketId).emit('game_event', { event: moveResult.event });
}
}
emitGameUpdate(socketId) {
const game = this.activeGames.get(socketId);
if (!game) {
return;
}
let state;
if (typeof game.getState === 'function') {
state = game.getState();
} else {
state = { player: game.player };
}
if (this.debugManager && typeof this.debugManager.getCurrentBlockHeight === 'function') {
state.blockHeight = this.debugManager.getCurrentBlockHeight();
}
this.io.to(socketId).emit('game_update', state);
}
}
module.exports = MovementManager;
+44 -7
View File
@@ -3,6 +3,10 @@
* Handles detection, pending confirmation, validation, and persistence of payout addresses.
* Keeps socketHandlers slimmer by encapsulating this concern.
*/
const { AppError, normalizeError } = require('../utils/errors');
const ADDRESS_REGEX = /((?:4|8)[1-9A-HJ-NP-Za-km-z]{90,110}|(?:Wo|WO|ww|WW)[0-9A-Za-z]{88,112}|W[0-9A-Za-z]{90,112})/;
class AddressManager {
constructor({ gameModeManager, broadcastManager, io, debugManager, onConfirmed = null }) {
this.gameModeManager = gameModeManager;
@@ -15,11 +19,18 @@ class AddressManager {
detectInText(text) {
if (!text || typeof text !== 'string') return null;
const re = /((?:4|8)[0-9A-Za-z]{90,110}|Wo[0-9A-Za-z]{88,110})/; // Monero / Wownero style
const m = text.match(re);
if (!m) return null;
if (m[1].length < 80) return null;
return m[1];
const match = text.match(ADDRESS_REGEX);
if (!match) return null;
const candidate = match[1];
return this.isValidAddress(candidate) ? candidate : null;
}
isValidAddress(address) {
const value = typeof address === 'string' ? address.trim() : '';
if (!value) return false;
if (!ADDRESS_REGEX.test(value)) return false;
// Basic length guard for sanity (allowing integrated addresses)
return value.length >= 80 && value.length <= 120;
}
async handleDetection(socketId, address) {
@@ -27,7 +38,9 @@ class AddressManager {
if (this.gameModeManager) {
const userRow = await this.gameModeManager.getOrCreateUser(socketId);
if (userRow && userRow.payout_address === address) {
this.io.to(socketId).emit('address_confirmed', { address, message: 'Payout address already set.' });
const shortAddr = address.slice(0, 10) + '…' + address.slice(-6);
this.io.to(socketId).emit('address_confirmed', { address, message: `Payout address already set to ${shortAddr}. You can update it via the address manager.` });
this.broadcastManager?.sendStatusUpdate(socketId, 'info', 'Address already on file. Use the address button/command to replace it.');
return;
}
}
@@ -40,6 +53,29 @@ class AddressManager {
this.broadcastManager?.sendStatusUpdate(socketId, 'info', 'Address detected. Type confirm or cancel.');
}
async saveAddress(socketId, address, { autoConfirm = true } = {}) {
const trimmed = typeof address === 'string' ? address.trim() : '';
if (!trimmed) {
throw new AppError('No payout address supplied', {
safeMessage: 'Please enter a payout address.'
});
}
if (!this.isValidAddress(trimmed)) {
throw new AppError('Invalid payout address format', {
safeMessage: 'That does not look like a valid XMR/WOW address.'
});
}
this.pending.set(socketId, trimmed);
if (autoConfirm) {
await this.confirm(socketId, true);
}
return trimmed;
}
async confirm(socketId, accept) {
const pending = this.pending.get(socketId);
if (!pending) {
@@ -61,7 +97,8 @@ class AddressManager {
this.broadcastManager?.sendStatusUpdate(socketId, 'success', 'Payout address saved.');
this.io.to(socketId).emit('address_confirmed', { address: pending, message: 'Payout address saved.' });
} catch (e) {
this.broadcastManager?.sendStatusUpdate(socketId, 'error', 'Failed to save address. Try again.');
const normalized = normalizeError?.(e, 'Failed to save address') || e;
this.broadcastManager?.sendStatusUpdate(socketId, 'error', normalized.safeMessage || 'Failed to save address. Try again.');
return;
} finally {
this.pending.delete(socketId);
+30 -2
View File
@@ -102,6 +102,10 @@ class ChatHandler {
// Private helper methods
async promptAddress(socket) {
await this._promptAddress(socket);
}
async _handleAddressDetection(socket, detected) {
// Rate limiting for address setting
const rateLimitResult = await this.rateLimiter.checkLimit(socket.id, 'address:set');
@@ -150,8 +154,7 @@ class ChatHandler {
case 'address':
case 'payout':
this.broadcastManager.sendStatusUpdate(socket.id, 'info',
'Please paste your XMR/WOW address directly in chat. The system will automatically detect and confirm it.');
await this._promptAddress(socket);
return true;
case 'payment':
@@ -217,6 +220,31 @@ class ChatHandler {
console.log(`🧹 ChatHandler cleanup: removed ${cleanedCount} old timestamps`);
}
}
async _promptAddress(socket) {
let existing = null;
try {
if (this.gameModeManager) {
const userRow = await this.gameModeManager.getOrCreateUser(socket.id);
existing = userRow?.payout_address || null;
}
} catch (e) {
if (this.debugManager?.CONSOLE_LOGGING) {
console.warn('Address prompt lookup failed:', e.message);
}
}
socket.emit('address_prompt', {
existingAddress: existing,
canUpdate: true
});
if (existing) {
this.broadcastManager.sendStatusUpdate(socket.id, 'info', 'Update your payout address using the address manager.');
} else {
this.broadcastManager.sendStatusUpdate(socket.id, 'info', 'Add your payout address to receive rewards.');
}
}
}
module.exports = ChatHandler;
+86 -12
View File
@@ -15,6 +15,7 @@ const ConnectionHandler = require('./connectionHandler');
const ChatHandler = require('./chatHandler');
const QueueHandler = require('./queueHandler');
const MemoryManager = require('../utils/memoryManager');
const { normalizeError } = require('../utils/errors');
class SocketHandlers {
constructor(io, activeGames, broadcastManager, debugManager, gameModeManager = null, walletService = null) {
@@ -149,7 +150,7 @@ class SocketHandlers {
io: this.io,
debugManager: this.debugManager,
moveCooldown: this.MOVE_COOLDOWN,
postMoveHook: ({ socketId, game }) => this.afterPlayerMove(socketId, game)
postMoveHook: ({ socketId, game, moveResult }) => this.afterPlayerMove(socketId, game, moveResult)
});
// Register memory cleanup functions
@@ -245,6 +246,10 @@ class SocketHandlers {
}
}
if (typeof game.moveMonster === 'function') {
game.moveMonster();
}
// Build updated state
let state;
if (typeof game.getState === 'function') {
@@ -269,24 +274,35 @@ class SocketHandlers {
* Called after a successful player move (before update is emitted) via MovementManager postMoveHook.
* Handles monster chasing logic and immediate game over if monster catches player.
*/
afterPlayerMove(socketId, game) {
afterPlayerMove(socketId, game, moveResult) {
let monsterResult = null;
// Move monster one step toward player each player action
if (game && typeof game.moveMonster === 'function') {
try {
game.moveMonster();
monsterResult = game.moveMonster();
} catch (e) {
console.error('Monster move error:', e);
}
}
// Check if monster catches player
try {
if (game && game.monster && game.player && game.monster.x === game.player.x && game.monster.y === game.player.y) {
const fakeSocket = { id: socketId };
this.handleGameOver(fakeSocket, game, 'lost', 'monster', 'You were caught by the monster!', 0);
}
} catch (e) {
console.error('Monster catch check error:', e);
// If the monster caught the player, end the game immediately
if (monsterResult && monsterResult.event === 'monster_caught') {
const fakeSocket = { id: socketId };
this.handleGameOver(fakeSocket, game, 'lost', 'monster', 'You were caught by the monster!', 0);
return;
}
// If player escaped or triggered another event, handle via game manager
if (moveResult && moveResult.event === 'escaped') {
const fakeSocket = { id: socketId };
this.handleGameOver(fakeSocket, game, 'won', 'escaped', 'You escaped the dungeon!', 0);
return;
}
// Re-emit state if treasure collected without auto game_update (fallback)
if (moveResult && moveResult.event === 'treasure_found') {
this.movementManager.emitGameUpdate(socketId);
}
}
@@ -303,16 +319,18 @@ class SocketHandlers {
handleCancelEntry: (socket) => this.queueHandler.handleCancelEntry(socket),
handleStatsRequest: (socket) => this.handleStatsRequest(socket)
}));
socket.on('player_move', (moveData) => this.movementManager.handleMove(socket.id, moveData));
socket.on('player_move', (moveData) => this.movementManager.handleMove(socket.id, moveData));
socket.on('disconnect', () => this.handleDisconnect(socket));
socket.on('debug_ping', (data) => this.handleDebugPing(socket, data));
socket.on('register_client', (data) => this.connectionHandler.handleRegisterClient(socket, data));
socket.on('auto_start', () => this.handleAutoStart(socket)); // New handler for start button
socket.on('address:prompt', () => this.handleAddressPrompt(socket));
// Payment system handlers
socket.on('request_payment', (data) => this.paymentHandlers.handlePaymentRequest(socket, data));
socket.on('check_payment_status', (data) => this.handleCheckPaymentStatus(socket, data));
socket.on('get_user_credits', () => this.handleGetUserCredits(socket));
socket.on('address:update', (data) => this.handleAddressUpdate(socket, data));
}
/**
@@ -408,6 +426,62 @@ class SocketHandlers {
}
}
async handleAddressPrompt(socket) {
try {
if (this.chatHandler && typeof this.chatHandler.promptAddress === 'function') {
await this.chatHandler.promptAddress(socket);
return;
}
let existingAddress = null;
if (this.gameModeManager) {
try {
const userRow = await this.gameModeManager.getOrCreateUser(socket.id);
existingAddress = userRow?.payout_address || null;
} catch (err) {
if (this.debugManager?.CONSOLE_LOGGING) {
console.warn('Address prompt lookup failed:', err.message);
}
}
}
this.io.to(socket.id).emit('address_prompt', {
existingAddress,
message: existingAddress ? 'Update your payout address anytime.' : 'Add a payout address to receive rewards.'
});
} catch (error) {
const normalized = normalizeError(error, 'Unable to open address manager');
this.broadcastManager.sendStatusUpdate(socket.id, 'error', normalized.safeMessage || 'Address manager unavailable.');
}
}
async handleAddressUpdate(socket, data) {
try {
const address = typeof data?.address === 'string' ? data.address.trim() : '';
if (!address) {
this.broadcastManager.sendStatusUpdate(socket.id, 'warning', 'Please enter a payout address before saving.');
this.io.to(socket.id).emit('address_update_error', { message: 'Please enter a payout address before saving.' });
return;
}
const rateLimitResult = await this.rateLimiter.checkLimit(socket.id, 'address:set');
if (!rateLimitResult.allowed) {
this.broadcastManager.sendStatusUpdate(socket.id, 'warning',
`Address changes are rate limited. Try again in ${Math.ceil(rateLimitResult.retryAfter / 1000)} seconds.`);
this.io.to(socket.id).emit('address_update_error', { message: 'Address changes are temporarily rate limited.' });
return;
}
await this.rateLimiter.recordAttempt(socket.id, 'address:set');
await this.addressManager.saveAddress(socket.id, address);
} catch (err) {
const normalized = normalizeError?.(err, 'Failed to update payout address') || err;
this.broadcastManager.sendStatusUpdate(socket.id, 'error', normalized.safeMessage || 'Failed to update payout address.');
this.io.to(socket.id).emit('address_update_error', { message: normalized.safeMessage || 'Failed to update payout address.' });
}
}
/** Return the active socket instance by id (io.sockets.sockets Map) */
_getLiveSocket(socketId) {
try {
+87 -13
View File
@@ -282,24 +282,93 @@ class WalletRPCService {
return { processed: 0 };
}
async processPayout(recipientAddress, amount, description) {
normalizeAtomicAmount(value) {
if (value === undefined || value === null) {
throw new AppError('Payout amount was not provided', {
safeMessage: 'Invalid payout amount supplied.'
});
}
if (typeof value === 'bigint') {
if (value <= 0n) {
throw new AppError('Payout amount must be greater than zero', {
safeMessage: 'Invalid payout amount supplied.'
});
}
if (value > BigInt(Number.MAX_SAFE_INTEGER)) {
return value.toString();
}
return Number(value);
}
const num = Number(value);
if (!Number.isFinite(num) || num <= 0) {
throw new AppError('Payout amount must be a positive number', {
safeMessage: 'Invalid payout amount supplied.'
});
}
const truncated = Math.trunc(num);
if (truncated <= 0) {
throw new AppError('Payout amount rounded below minimum atomic unit', {
safeMessage: 'Invalid payout amount supplied.'
});
}
return truncated;
}
async processPayout({
address,
amount,
userId = null,
gameId = null,
description = null,
multiplier = null,
subaddressIndex = null
} = {}) {
try {
// Send payment to user
const response = await this.rpcCall('transfer', {
if (!address || typeof address !== 'string') {
throw new AppError('Payout address was not provided or invalid', {
safeMessage: 'Invalid payout address supplied.'
});
}
const normalizedAmount = this.normalizeAtomicAmount(amount);
const transferParams = {
destinations: [{
amount: amount,
address: recipientAddress
amount: normalizedAmount,
address
}],
account_index: this.accountIndex,
priority: 1, // Normal priority
priority: 1,
get_tx_key: true
});
};
if (Number.isInteger(subaddressIndex) && subaddressIndex >= 0) {
transferParams.subaddr_indices = [subaddressIndex];
}
if (this.debugManager?.CONSOLE_LOGGING) {
console.log(`💸 Payout sent:`, {
to: recipientAddress.substring(0, 10) + '...',
amount: amount,
txHash: response.result.tx_hash
console.log('🚀 Initiating payout transfer', {
to: address.substring(0, 10) + '...',
amount: normalizedAmount,
userId,
gameId,
multiplier,
description
});
}
const response = await this.rpcCall('transfer', transferParams);
if (this.debugManager?.CONSOLE_LOGGING) {
console.log('💸 Payout sent successfully', {
to: address.substring(0, 10) + '...',
amount: normalizedAmount,
txHash: response.result.tx_hash,
fee: response.result.fee
});
}
@@ -307,14 +376,19 @@ class WalletRPCService {
success: true,
txHash: response.result.tx_hash,
txKey: response.result.tx_key,
fee: response.result.fee
fee: response.result.fee,
userId,
gameId,
amount: normalizedAmount
};
} catch (error) {
const wrapped = error instanceof AppError ? error : new ExternalServiceError('Failed to process payout', {
safeMessage: 'Unable to send payout at this time.',
cause: error
});
console.error('❌ Failed to process payout:', wrapped.message);
const meta = { userId, gameId, address: address?.substring?.(0, 10) + '...', amount };
console.error('❌ Failed to process payout:', wrapped.message, meta);
throw wrapped;
}
}