mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-17 22:31:26 +02:00
add db commands and test payout (worked!)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Wowngeon
|
||||
# Wownerogue
|
||||
|
||||
Wowngeon is a browser-based roguelike that synchronizes dungeon runs with Monero (XMR) and Wownero (WOW) block timing. The backend is a Node.js/Express server with Socket.IO for realtime play, optional payment enforcement, and automated payouts.
|
||||
Wownerogue is a browser-based roguelike that synchronizes dungeon runs with Monero (XMR) and Wownero (WOW) block timing. The backend is a Node.js/Express server with Socket.IO for realtime play, optional payment enforcement, and automated payouts.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -68,7 +68,7 @@ Only the payment routes require wallet RPC access; all other gameplay functions
|
||||
1. Clone and enter the server directory.
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd wowngeon/src
|
||||
cd wownerogue/src
|
||||
```
|
||||
2. Install dependencies.
|
||||
```bash
|
||||
@@ -79,9 +79,13 @@ Only the payment routes require wallet RPC access; all other gameplay functions
|
||||
cp .env.example .env
|
||||
# edit .env to match your database and wallet settings
|
||||
```
|
||||
4. Provision the database (migrations run automatically at startup).
|
||||
4. Provision the database.
|
||||
```bash
|
||||
createdb wowngeon
|
||||
# Create database and user
|
||||
npm run db:create
|
||||
|
||||
# (Optional) To reset the database later:
|
||||
# npm run db:reset
|
||||
```
|
||||
5. Start the development server.
|
||||
```bash
|
||||
@@ -152,7 +156,7 @@ RPC_POLL_INTERVAL=2000
|
||||
# Database
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_NAME=wowgue
|
||||
DB_NAME=wownerogue
|
||||
DB_USER=jw
|
||||
DB_PASSWORD=jw
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# Wownerogue Development Todo
|
||||
|
||||
## High Priority
|
||||
- [ ] **Verify Server Startup**: Ensure `npm run dev` starts cleanly without errors after recent refactors.
|
||||
- [ ] **Test Payment Flow**: Verify the full payment lifecycle (Request -> QR -> Payment -> Confirmation -> Game Start).
|
||||
- [ ] **Test Payouts**: Confirm payouts are triggered correctly upon winning in paid modes.
|
||||
- [ ] **Address Persistence**: Verify that payout addresses persist across server restarts via session tokens.
|
||||
|
||||
## Improvements / Refactoring
|
||||
- [ ] **Error Handling**: Extend `normalizeError` usage to remaining network modules for consistent error reporting.
|
||||
- [ ] **Configuration Persistence**: Implement persistence for payment configuration updates to support hot reloads.
|
||||
- [ ] **Monitoring Dashboard**: Build a lightweight dashboard for live metrics (active games, queue length, rate limits).
|
||||
- [ ] **Frontend Cleanup**: Remove any remaining dead code or unused assets from the `html/` directory.
|
||||
|
||||
## Documentation
|
||||
- [ ] **API Documentation**: Expand API documentation in `README.md` with request/response examples.
|
||||
- [ ] **Deployment Guide**: Create a detailed deployment guide including reverse proxy setup (Nginx/Caddy).
|
||||
@@ -1,812 +0,0 @@
|
||||
I'll first check if we've completed #4 (database indexes), then provide a comprehensive plan for the unified payment mode.
|
||||
|
||||
## 📊 Checking Database Indexes Status
|
||||
|
||||
Let me check what indexes have been applied:
|
||||
|
||||
```bash
|
||||
# Check the migration file we created
|
||||
cat src/migrations/004_security_improvements.sql | grep -E "CREATE INDEX|ALTER TABLE.*CONSTRAINT"
|
||||
```
|
||||
|
||||
Looking at the migration file, we have:
|
||||
- ✅ Created indexes for: games(user_id), games(status), payments(status), payments(created_at), users(last_seen)
|
||||
- ✅ Added foreign key constraints for referential integrity
|
||||
- ✅ Added audit_log and credit_transactions tables
|
||||
- ⚠️ The migration had issues with CONCURRENTLY keyword (fixed by removing it)
|
||||
|
||||
**Status: #4 is PARTIALLY complete** - The indexes exist in the migration but we need to verify they were actually applied to the database.
|
||||
|
||||
## 🎯 Unified Payment Mode - Comprehensive Design Plan
|
||||
|
||||
After deep review of the codebase, here's my detailed plan for merging PAID_SINGLE and PAID_CREDITS into a unified, flexible payment system:
|
||||
|
||||
### 📋 Current State Analysis
|
||||
|
||||
**PAID_SINGLE Mode:**
|
||||
- User pays per game
|
||||
- Payment required for each game
|
||||
- Payouts: 2x for escape, 3x for escape with treasure
|
||||
- No credit system
|
||||
- Simple flow: Pay → Play → Win/Lose → Payout
|
||||
|
||||
**PAID_CREDITS Mode:**
|
||||
- User buys credit packages (10 credits for X WOW)
|
||||
- Each game costs 1 credit
|
||||
- Currently: Optional payouts (CREDITS_PAYOUT_ENABLED flag)
|
||||
- Complex flow: Buy Credits → Play Multiple Games → Optional Payouts
|
||||
|
||||
**Code Locations:**
|
||||
- Mode logic: gameModeManager.js
|
||||
- Payment handling: paymentHandlers.js
|
||||
- Queue management: queueManager.js
|
||||
- Configuration: `.env` file with GAME_MODE, CREDITS_PAYOUT_ENABLED
|
||||
|
||||
### 🏗️ Unified Payment Architecture
|
||||
|
||||
```javascript
|
||||
// New Unified Payment Configuration Structure
|
||||
const UNIFIED_PAYMENT_CONFIG = {
|
||||
// Global payment system toggle
|
||||
paymentsEnabled: true,
|
||||
|
||||
// Currency configuration
|
||||
currency: {
|
||||
symbol: 'WOW',
|
||||
decimals: 11,
|
||||
minPayment: BigInt('1e10'), // 0.01 WOW minimum
|
||||
maxPayment: BigInt('1e14') // 1000 WOW maximum
|
||||
},
|
||||
|
||||
// Payment modes (both can be enabled simultaneously)
|
||||
modes: {
|
||||
// Direct pay-per-game mode
|
||||
direct: {
|
||||
enabled: true,
|
||||
price: BigInt('1e11'), // 0.1 WOW per game
|
||||
requiresAddress: true,
|
||||
allowGuestPlay: false
|
||||
},
|
||||
|
||||
// Credit package mode
|
||||
credits: {
|
||||
enabled: true,
|
||||
creditsPerGame: 1,
|
||||
requiresAddress: true,
|
||||
packages: [
|
||||
{ id: 'small', credits: 10, price: BigInt('5e11'), bonus: 0 },
|
||||
{ id: 'medium', credits: 25, price: BigInt('10e11'), bonus: 2 },
|
||||
{ id: 'large', credits: 100, price: BigInt('35e11'), bonus: 15 }
|
||||
],
|
||||
// Allow mixing - can buy credits even in direct mode
|
||||
allowMixedMode: true
|
||||
}
|
||||
},
|
||||
|
||||
// Payout configuration (applies to both modes)
|
||||
payouts: {
|
||||
enabled: true,
|
||||
requiresKYC: false, // Future: regulatory compliance
|
||||
|
||||
// Payout rules by mode
|
||||
rules: {
|
||||
direct: {
|
||||
enabled: true,
|
||||
multipliers: {
|
||||
escape: 2.0,
|
||||
escapeWithTreasure: 3.0,
|
||||
perfectRun: 5.0 // Future: no damage, fast time
|
||||
},
|
||||
minPayout: BigInt('1e10'), // 0.01 WOW minimum
|
||||
maxPayout: BigInt('1e13') // 10 WOW maximum per game
|
||||
},
|
||||
credits: {
|
||||
enabled: true,
|
||||
// Different multipliers for credit mode (operator choice)
|
||||
multipliers: {
|
||||
escape: 1.5,
|
||||
escapeWithTreasure: 2.0,
|
||||
perfectRun: 3.0
|
||||
},
|
||||
// Credits mode uses base price for calculations
|
||||
baseValue: BigInt('5e10'), // 0.05 WOW per credit
|
||||
minPayout: BigInt('1e10'),
|
||||
maxPayout: BigInt('5e12') // 5 WOW maximum per game
|
||||
}
|
||||
},
|
||||
|
||||
// Payout processing
|
||||
processing: {
|
||||
automatic: true,
|
||||
batchingEnabled: true,
|
||||
batchInterval: 300, // 5 minutes
|
||||
confirmations: 1,
|
||||
maxRetries: 3
|
||||
}
|
||||
},
|
||||
|
||||
// Promotional features
|
||||
promotions: {
|
||||
enabled: true,
|
||||
freeCredits: {
|
||||
onSignup: 0,
|
||||
onFirstPurchase: 2,
|
||||
referralBonus: 5
|
||||
},
|
||||
discounts: {
|
||||
enabled: false,
|
||||
bulkDiscount: 0.1, // 10% off large packages
|
||||
happyHour: null // Future: time-based discounts
|
||||
}
|
||||
},
|
||||
|
||||
// Anti-abuse measures
|
||||
limits: {
|
||||
maxGamesPerHour: 60,
|
||||
maxPayoutsPerDay: 100,
|
||||
maxCreditPurchasePerDay: BigInt('1e14'), // 1000 WOW
|
||||
cooldownBetweenGames: 5 // seconds
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 📁 File Structure Refactoring
|
||||
|
||||
```
|
||||
src/
|
||||
├── payments/
|
||||
│ ├── UnifiedPaymentManager.js # NEW: Main payment orchestrator
|
||||
│ ├── PaymentModeManager.js # NEW: Handles mode logic
|
||||
│ ├── CreditManager.js # NEW: Credit operations
|
||||
│ ├── PayoutManager.js # NEW: Payout processing
|
||||
│ ├── PaymentValidator.js # NEW: Input validation
|
||||
│ ├── moneroPayService.js # EXISTING: Keep
|
||||
│ ├── qrService.js # EXISTING: Keep
|
||||
│ └── walletRPCService.js # EXISTING: Keep
|
||||
├── game/
|
||||
│ ├── gameModeManager.js # REFACTOR: Simplify to game logic only
|
||||
│ └── ...
|
||||
└── config/
|
||||
├── paymentConfig.js # NEW: Centralized config
|
||||
└── environmentValidator.js # NEW: Validate .env settings
|
||||
```
|
||||
|
||||
### 🔄 Migration Strategy
|
||||
|
||||
#### Phase 1: Database Schema Updates (Day 1)
|
||||
```sql
|
||||
-- New migrations/005_unified_payment_system.sql
|
||||
|
||||
-- Add payment mode tracking
|
||||
ALTER TABLE payments ADD COLUMN payment_mode VARCHAR(20) DEFAULT 'direct';
|
||||
ALTER TABLE payments ADD COLUMN credit_package_id VARCHAR(20);
|
||||
ALTER TABLE payments ADD COLUMN credits_purchased INTEGER DEFAULT 0;
|
||||
|
||||
-- Add user preferences
|
||||
ALTER TABLE users ADD COLUMN preferred_payment_mode VARCHAR(20) DEFAULT 'direct';
|
||||
ALTER TABLE users ADD COLUMN total_credits_purchased INTEGER DEFAULT 0;
|
||||
ALTER TABLE users ADD COLUMN total_games_played INTEGER DEFAULT 0;
|
||||
ALTER TABLE users ADD COLUMN total_payouts_received BIGINT DEFAULT 0;
|
||||
|
||||
-- Add payout tracking improvements
|
||||
CREATE TABLE IF NOT EXISTS payouts (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER REFERENCES users(id),
|
||||
game_id INTEGER REFERENCES games(id),
|
||||
amount BIGINT NOT NULL,
|
||||
multiplier DECIMAL(3,1) NOT NULL,
|
||||
reason VARCHAR(50) NOT NULL,
|
||||
status VARCHAR(20) DEFAULT 'pending',
|
||||
tx_hash VARCHAR(64),
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
processed_at TIMESTAMP,
|
||||
error_message TEXT
|
||||
);
|
||||
|
||||
-- Add credit transaction types
|
||||
ALTER TABLE credit_transactions ADD COLUMN transaction_type VARCHAR(20);
|
||||
-- Types: 'purchase', 'game_fee', 'payout', 'bonus', 'refund', 'adjustment'
|
||||
|
||||
-- Indexes for performance
|
||||
CREATE INDEX idx_payouts_status ON payouts(status);
|
||||
CREATE INDEX idx_payouts_user_id ON payouts(user_id);
|
||||
CREATE INDEX idx_payments_payment_mode ON payments(payment_mode);
|
||||
```
|
||||
|
||||
#### Phase 2: Configuration System (Day 1-2)
|
||||
|
||||
```javascript
|
||||
// src/config/paymentConfig.js
|
||||
class PaymentConfigManager {
|
||||
constructor() {
|
||||
this.config = this.loadConfig();
|
||||
this.validateConfig();
|
||||
}
|
||||
|
||||
loadConfig() {
|
||||
// Priority: Environment > Database > Defaults
|
||||
const config = { ...DEFAULT_CONFIG };
|
||||
|
||||
// Load from environment
|
||||
if (process.env.PAYMENT_MODES) {
|
||||
const modes = process.env.PAYMENT_MODES.split(',');
|
||||
config.modes.direct.enabled = modes.includes('direct');
|
||||
config.modes.credits.enabled = modes.includes('credits');
|
||||
}
|
||||
|
||||
// Payouts configuration
|
||||
config.payouts.enabled = process.env.PAYOUTS_ENABLED === 'true';
|
||||
config.payouts.rules.direct.enabled =
|
||||
process.env.DIRECT_PAYOUTS_ENABLED !== 'false';
|
||||
config.payouts.rules.credits.enabled =
|
||||
process.env.CREDIT_PAYOUTS_ENABLED === 'true';
|
||||
|
||||
// Price configuration
|
||||
if (process.env.GAME_PRICE) {
|
||||
config.modes.direct.price = BigInt(process.env.GAME_PRICE);
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
validateConfig() {
|
||||
// Ensure at least one payment mode is enabled
|
||||
if (!this.config.modes.direct.enabled &&
|
||||
!this.config.modes.credits.enabled) {
|
||||
throw new Error('At least one payment mode must be enabled');
|
||||
}
|
||||
|
||||
// Validate payout configuration
|
||||
if (this.config.payouts.enabled) {
|
||||
if (!this.config.modes.direct.enabled &&
|
||||
!this.config.payouts.rules.credits.enabled) {
|
||||
console.warn('Payouts enabled but no payout rules active');
|
||||
}
|
||||
}
|
||||
|
||||
// Validate prices
|
||||
this.validatePrices();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
validatePrices() {
|
||||
const { direct, credits } = this.config.modes;
|
||||
|
||||
if (direct.enabled && direct.price <= 0n) {
|
||||
throw new Error('Direct game price must be positive');
|
||||
}
|
||||
|
||||
if (credits.enabled) {
|
||||
for (const pkg of credits.packages) {
|
||||
if (pkg.price <= 0n) {
|
||||
throw new Error(`Credit package ${pkg.id} has invalid price`);
|
||||
}
|
||||
if (pkg.credits <= 0) {
|
||||
throw new Error(`Credit package ${pkg.id} has invalid credit count`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dynamic configuration updates
|
||||
async updateConfig(updates) {
|
||||
// Validate updates
|
||||
const newConfig = { ...this.config, ...updates };
|
||||
|
||||
// Test configuration
|
||||
const testManager = new PaymentConfigManager();
|
||||
testManager.config = newConfig;
|
||||
testManager.validateConfig();
|
||||
|
||||
// Apply updates
|
||||
this.config = newConfig;
|
||||
|
||||
// Persist to database for next restart
|
||||
await this.persistConfig();
|
||||
|
||||
// Notify all connected clients
|
||||
this.broadcastConfigUpdate();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Phase 3: Unified Payment Manager (Day 2-3)
|
||||
|
||||
```javascript
|
||||
// src/payments/UnifiedPaymentManager.js
|
||||
class UnifiedPaymentManager {
|
||||
constructor(config, walletService, db) {
|
||||
this.config = config;
|
||||
this.wallet = walletService;
|
||||
this.db = db;
|
||||
|
||||
this.creditManager = new CreditManager(db, config);
|
||||
this.payoutManager = new PayoutManager(wallet, db, config);
|
||||
this.validator = new PaymentValidator(config);
|
||||
|
||||
this.pendingPayments = new Map();
|
||||
this.activeGames = new Map();
|
||||
}
|
||||
|
||||
// Unified game start flow
|
||||
async requestGameStart(userId, socketId) {
|
||||
const user = await this.db.getUser(userId);
|
||||
|
||||
// Check if user has credits
|
||||
if (user.credits > 0 && this.config.modes.credits.enabled) {
|
||||
return this.startGameWithCredits(user, socketId);
|
||||
}
|
||||
|
||||
// Check if direct payment is enabled
|
||||
if (this.config.modes.direct.enabled) {
|
||||
return this.startGameWithDirectPayment(user, socketId);
|
||||
}
|
||||
|
||||
// Offer credit purchase if only credits mode
|
||||
if (this.config.modes.credits.enabled) {
|
||||
return {
|
||||
action: 'purchase_credits',
|
||||
packages: this.config.modes.credits.packages
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error('No payment method available');
|
||||
}
|
||||
|
||||
async startGameWithCredits(user, socketId) {
|
||||
// Validate credit balance
|
||||
if (user.credits < this.config.modes.credits.creditsPerGame) {
|
||||
return {
|
||||
action: 'insufficient_credits',
|
||||
required: this.config.modes.credits.creditsPerGame,
|
||||
balance: user.credits,
|
||||
packages: this.config.modes.credits.packages
|
||||
};
|
||||
}
|
||||
|
||||
// Deduct credits
|
||||
await this.creditManager.deductCredits(
|
||||
user.id,
|
||||
this.config.modes.credits.creditsPerGame,
|
||||
'game_start'
|
||||
);
|
||||
|
||||
// Start game
|
||||
return {
|
||||
action: 'game_start',
|
||||
mode: 'credits',
|
||||
creditsRemaining: user.credits - this.config.modes.credits.creditsPerGame
|
||||
};
|
||||
}
|
||||
|
||||
async startGameWithDirectPayment(user, socketId) {
|
||||
// Check if user needs to set payout address
|
||||
if (this.config.payouts.enabled && !user.payout_address) {
|
||||
return {
|
||||
action: 'require_payout_address',
|
||||
message: 'Please set your payout address first'
|
||||
};
|
||||
}
|
||||
|
||||
// Generate payment request
|
||||
const payment = await this.createPaymentRequest(
|
||||
user.id,
|
||||
this.config.modes.direct.price,
|
||||
'direct_game'
|
||||
);
|
||||
|
||||
return {
|
||||
action: 'payment_required',
|
||||
payment: {
|
||||
address: payment.address,
|
||||
amount: payment.amount,
|
||||
qr: payment.qr,
|
||||
expires: payment.expires
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Handle game completion and payouts
|
||||
async processGameCompletion(gameId, outcome) {
|
||||
const game = await this.db.getGame(gameId);
|
||||
const user = await this.db.getUser(game.user_id);
|
||||
|
||||
// Determine if payout is due
|
||||
const payoutConfig = this.getPayoutConfig(game.payment_mode);
|
||||
if (!payoutConfig.enabled) {
|
||||
return { payout: false };
|
||||
}
|
||||
|
||||
// Calculate payout
|
||||
const payout = this.calculatePayout(game, outcome, payoutConfig);
|
||||
if (payout.amount <= 0n) {
|
||||
return { payout: false };
|
||||
}
|
||||
|
||||
// Process payout
|
||||
const result = await this.payoutManager.processPayout(
|
||||
user,
|
||||
game,
|
||||
payout
|
||||
);
|
||||
|
||||
return {
|
||||
payout: true,
|
||||
amount: payout.amount,
|
||||
multiplier: payout.multiplier,
|
||||
status: result.status,
|
||||
txHash: result.txHash
|
||||
};
|
||||
}
|
||||
|
||||
calculatePayout(game, outcome, config) {
|
||||
let multiplier = 0;
|
||||
let baseAmount = 0n;
|
||||
|
||||
// Determine base amount
|
||||
if (game.payment_mode === 'direct') {
|
||||
baseAmount = this.config.modes.direct.price;
|
||||
} else if (game.payment_mode === 'credits') {
|
||||
baseAmount = this.config.payouts.rules.credits.baseValue;
|
||||
}
|
||||
|
||||
// Determine multiplier
|
||||
if (outcome.reason === 'escaped') {
|
||||
if (outcome.treasure) {
|
||||
multiplier = config.multipliers.escapeWithTreasure;
|
||||
} else {
|
||||
multiplier = config.multipliers.escape;
|
||||
}
|
||||
|
||||
// Future: Perfect run bonus
|
||||
if (outcome.perfectRun) {
|
||||
multiplier = config.multipliers.perfectRun;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate final amount
|
||||
const amount = BigInt(Math.floor(Number(baseAmount) * multiplier));
|
||||
|
||||
// Apply limits
|
||||
const minPayout = config.minPayout;
|
||||
const maxPayout = config.maxPayout;
|
||||
|
||||
if (amount < minPayout) return { amount: 0n, multiplier: 0 };
|
||||
if (amount > maxPayout) return { amount: maxPayout, multiplier };
|
||||
|
||||
return { amount, multiplier };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Phase 4: Credit Management System (Day 3)
|
||||
|
||||
```javascript
|
||||
// src/payments/CreditManager.js
|
||||
class CreditManager {
|
||||
constructor(db, config) {
|
||||
this.db = db;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
async purchaseCredits(userId, packageId, paymentId) {
|
||||
const pkg = this.config.modes.credits.packages.find(p => p.id === packageId);
|
||||
if (!pkg) {
|
||||
throw new Error('Invalid credit package');
|
||||
}
|
||||
|
||||
const client = await this.db.pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// Add credits to user account
|
||||
const totalCredits = pkg.credits + pkg.bonus;
|
||||
await client.query(
|
||||
`UPDATE users
|
||||
SET credits = credits + $1,
|
||||
total_credits_purchased = total_credits_purchased + $2
|
||||
WHERE id = $3`,
|
||||
[totalCredits, pkg.credits, userId]
|
||||
);
|
||||
|
||||
// Log transaction
|
||||
await client.query(
|
||||
`INSERT INTO credit_transactions
|
||||
(user_id, amount, transaction_type, reason, payment_id)
|
||||
VALUES ($1, $2, $3, $4, $5)`,
|
||||
[userId, totalCredits, 'purchase', `Package: ${packageId}`, paymentId]
|
||||
);
|
||||
|
||||
// Add bonus credits if applicable
|
||||
if (pkg.bonus > 0) {
|
||||
await client.query(
|
||||
`INSERT INTO credit_transactions
|
||||
(user_id, amount, transaction_type, reason)
|
||||
VALUES ($1, $2, $3, $4)`,
|
||||
[userId, pkg.bonus, 'bonus', `Package ${packageId} bonus`]
|
||||
);
|
||||
}
|
||||
|
||||
await client.query('COMMIT');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
creditsAdded: totalCredits,
|
||||
newBalance: await this.getBalance(userId)
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async deductCredits(userId, amount, reason) {
|
||||
const client = await this.db.pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// Check balance
|
||||
const balance = await this.getBalance(userId);
|
||||
if (balance < amount) {
|
||||
throw new Error('Insufficient credits');
|
||||
}
|
||||
|
||||
// Deduct credits
|
||||
await client.query(
|
||||
`UPDATE users
|
||||
SET credits = credits - $1,
|
||||
total_games_played = total_games_played + 1
|
||||
WHERE id = $2`,
|
||||
[amount, userId]
|
||||
);
|
||||
|
||||
// Log transaction
|
||||
await client.query(
|
||||
`INSERT INTO credit_transactions
|
||||
(user_id, amount, transaction_type, reason)
|
||||
VALUES ($1, $2, $3, $4)`,
|
||||
[userId, -amount, 'game_fee', reason]
|
||||
);
|
||||
|
||||
await client.query('COMMIT');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
newBalance: balance - amount
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async refundCredits(userId, amount, reason) {
|
||||
// Similar to purchaseCredits but for refunds
|
||||
}
|
||||
|
||||
async getBalance(userId) {
|
||||
const result = await this.db.query(
|
||||
'SELECT credits FROM users WHERE id = $1',
|
||||
[userId]
|
||||
);
|
||||
return result.rows[0]?.credits || 0;
|
||||
}
|
||||
|
||||
async getCreditHistory(userId, limit = 50) {
|
||||
const result = await this.db.query(
|
||||
`SELECT * FROM credit_transactions
|
||||
WHERE user_id = $1
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $2`,
|
||||
[userId, limit]
|
||||
);
|
||||
return result.rows;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Phase 5: UI/UX Updates (Day 4)
|
||||
|
||||
```javascript
|
||||
// Frontend changes needed
|
||||
// html/js/network/socketHandlers.js additions
|
||||
|
||||
SocketHandlers.onPaymentModeSelection = function(data) {
|
||||
// Show available payment options
|
||||
if (data.modes.length === 1) {
|
||||
// Single mode - proceed directly
|
||||
this.proceedWithPaymentMode(data.modes[0]);
|
||||
} else {
|
||||
// Multiple modes - show selection UI
|
||||
UI.showPaymentModeSelection(data.modes);
|
||||
}
|
||||
};
|
||||
|
||||
SocketHandlers.onCreditPackageSelection = function(data) {
|
||||
// Show credit packages
|
||||
const packages = data.packages.map(pkg => ({
|
||||
id: pkg.id,
|
||||
credits: pkg.credits + (pkg.bonus || 0),
|
||||
price: UI.formatCurrency(pkg.price),
|
||||
bonus: pkg.bonus > 0 ? `+${pkg.bonus} bonus!` : '',
|
||||
savings: this.calculateSavings(pkg)
|
||||
}));
|
||||
|
||||
UI.showCreditPackages(packages);
|
||||
};
|
||||
|
||||
SocketHandlers.onPayoutProcessed = function(data) {
|
||||
// Show payout notification
|
||||
const message = `Payout processed: ${UI.formatCurrency(data.amount)} (${data.multiplier}x)`;
|
||||
|
||||
UI.showNotification(message, 'success');
|
||||
|
||||
// Play appropriate sound
|
||||
if (data.multiplier >= 3) {
|
||||
AudioAlerts.playFile('big_win');
|
||||
} else {
|
||||
AudioAlerts.playFile('payout_received');
|
||||
}
|
||||
};
|
||||
|
||||
// UI additions
|
||||
UI.showPaymentModeSelection = function(modes) {
|
||||
const html = `
|
||||
<div class="payment-mode-selection">
|
||||
<h3>Select Payment Method</h3>
|
||||
${modes.map(mode => `
|
||||
<div class="payment-option" data-mode="${mode.id}">
|
||||
<h4>${mode.name}</h4>
|
||||
<p>${mode.description}</p>
|
||||
${mode.price ? `<p class="price">${UI.formatCurrency(mode.price)}</p>` : ''}
|
||||
${mode.creditsAvailable ? `<p class="credits">Credits: ${mode.creditsAvailable}</p>` : ''}
|
||||
<button onclick="UI.selectPaymentMode('${mode.id}')">
|
||||
${mode.buttonText}
|
||||
</button>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.showModal(html);
|
||||
};
|
||||
```
|
||||
|
||||
### 🔧 Environment Configuration
|
||||
|
||||
```bash
|
||||
# New .env configuration
|
||||
# Payment System Configuration
|
||||
PAYMENTS_ENABLED=true
|
||||
|
||||
# Payment Modes (comma-separated: direct,credits)
|
||||
PAYMENT_MODES=direct,credits
|
||||
|
||||
# Direct Payment Configuration
|
||||
DIRECT_PAYMENT_ENABLED=true
|
||||
DIRECT_GAME_PRICE=100000000000 # 0.1 WOW
|
||||
DIRECT_PAYOUTS_ENABLED=true
|
||||
DIRECT_PAYOUT_ESCAPE=2.0
|
||||
DIRECT_PAYOUT_TREASURE=3.0
|
||||
|
||||
# Credit System Configuration
|
||||
CREDITS_ENABLED=true
|
||||
CREDITS_PER_GAME=1
|
||||
CREDITS_PAYOUTS_ENABLED=false # Operator choice
|
||||
CREDITS_PAYOUT_BASE=50000000000 # 0.05 WOW per credit
|
||||
CREDITS_PAYOUT_ESCAPE=1.5
|
||||
CREDITS_PAYOUT_TREASURE=2.0
|
||||
|
||||
# Credit Packages (JSON format)
|
||||
CREDITS_PACKAGES='[
|
||||
{"id":"small","credits":10,"price":"500000000000","bonus":0},
|
||||
{"id":"medium","credits":25,"price":"1000000000000","bonus":2},
|
||||
{"id":"large","credits":100,"price":"3500000000000","bonus":15}
|
||||
]'
|
||||
|
||||
# Mixed Mode Settings
|
||||
ALLOW_MIXED_MODE=true # Can use credits even in direct mode
|
||||
PREFER_CREDITS_FIRST=true # Use credits before requesting payment
|
||||
|
||||
# Payout Processing
|
||||
PAYOUT_BATCH_ENABLED=true
|
||||
PAYOUT_BATCH_INTERVAL=300 # 5 minutes
|
||||
PAYOUT_MIN_AMOUNT=10000000000 # 0.01 WOW minimum
|
||||
PAYOUT_MAX_PER_GAME=10000000000000 # 10 WOW maximum
|
||||
|
||||
# Anti-Abuse Limits
|
||||
MAX_GAMES_PER_HOUR=60
|
||||
MAX_PAYOUTS_PER_DAY=100
|
||||
GAME_COOLDOWN_SECONDS=5
|
||||
```
|
||||
|
||||
### 📈 Migration Timeline
|
||||
|
||||
**Week 1:**
|
||||
- Day 1: Database schema updates, configuration system
|
||||
- Day 2: UnifiedPaymentManager core implementation
|
||||
- Day 3: CreditManager and PayoutManager
|
||||
- Day 4: Frontend UI updates
|
||||
- Day 5: Integration testing
|
||||
|
||||
**Week 2:**
|
||||
- Day 1-2: Migrate existing data
|
||||
- Day 3: Staging deployment and testing
|
||||
- Day 4: Performance optimization
|
||||
- Day 5: Production deployment
|
||||
|
||||
### ✅ Testing Strategy
|
||||
|
||||
```javascript
|
||||
// Test scenarios to cover
|
||||
describe('Unified Payment System', () => {
|
||||
test('Direct payment only mode', async () => {
|
||||
const config = { modes: { direct: { enabled: true }, credits: { enabled: false }}};
|
||||
// Test direct payment flow
|
||||
});
|
||||
|
||||
test('Credits only mode', async () => {
|
||||
const config = { modes: { direct: { enabled: false }, credits: { enabled: true }}};
|
||||
// Test credit purchase and usage
|
||||
});
|
||||
|
||||
test('Mixed mode - prefer credits', async () => {
|
||||
const config = {
|
||||
modes: { direct: { enabled: true }, credits: { enabled: true }},
|
||||
preferCreditsFirst: true
|
||||
};
|
||||
// Test credits used before direct payment
|
||||
});
|
||||
|
||||
test('Payout calculations - direct mode', async () => {
|
||||
// Test 2x and 3x multipliers
|
||||
});
|
||||
|
||||
test('Payout calculations - credits mode', async () => {
|
||||
// Test different multipliers for credits
|
||||
});
|
||||
|
||||
test('Anti-abuse limits', async () => {
|
||||
// Test rate limiting and max limits
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 🎯 Benefits of This Approach
|
||||
|
||||
1. **Maximum Flexibility**: Operators can enable/disable any combination
|
||||
2. **Backward Compatible**: Existing configurations continue to work
|
||||
3. **Future Proof**: Easy to add new payment methods or payout rules
|
||||
4. **Granular Control**: Different payout rates for different modes
|
||||
5. **Player Choice**: Users can choose their preferred payment method
|
||||
6. **Revenue Optimization**: Credit packages encourage bulk purchases
|
||||
7. **Risk Management**: Configurable limits and anti-abuse measures
|
||||
|
||||
### 🚀 Implementation Priority
|
||||
|
||||
1. **High Priority (Days 1-3)**:
|
||||
- UnifiedPaymentManager
|
||||
- Configuration system
|
||||
- Database migrations
|
||||
- Basic mode switching
|
||||
|
||||
2. **Medium Priority (Days 4-5)**:
|
||||
- Credit management
|
||||
- Payout processing
|
||||
- UI updates
|
||||
- Testing
|
||||
|
||||
3. **Low Priority (Future)**:
|
||||
- Advanced analytics
|
||||
- A/B testing framework
|
||||
- Dynamic pricing
|
||||
- Loyalty rewards
|
||||
|
||||
This plan provides a robust, flexible payment system that can adapt to different business models while maintaining code simplicity and maintainability. The operator gains complete control over payment modes, pricing, and payout rules through simple configuration changes.
|
||||
Binary file not shown.
+26
-18
@@ -1,7 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Wowgue - A Wownero Roguelike</title>
|
||||
<title>Wownerogue - A Wownero Roguelike</title>
|
||||
<link rel="stylesheet" type="text/css" href="styles/stylesheet.css">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
(function(){
|
||||
let opts = {};
|
||||
try {
|
||||
const existing = localStorage.getItem('wowngeon_token');
|
||||
const existing = localStorage.getItem('wownerogue_token');
|
||||
if (existing) {
|
||||
opts.query = { resumeToken: existing };
|
||||
}
|
||||
@@ -38,6 +38,7 @@
|
||||
|
||||
<!-- Other modules -->
|
||||
<script src="js/ui/ui.js"></script>
|
||||
<script src="js/ui/paymentUI.js"></script>
|
||||
<script src="js/ui/audioAlerts.js"></script>
|
||||
<script src="js/ui/addressModal.js"></script>
|
||||
<script src="js/network/socketHandlers.js"></script>
|
||||
@@ -59,6 +60,9 @@
|
||||
<button id="startButton" style="background: #0a5c0a; color: #ff0; border: 2px solid #0f0; padding: 8px 16px; font-weight: bold; cursor: pointer; margin-top: 5px; display: none;">
|
||||
🎮 START GAME
|
||||
</button>
|
||||
<button id="shopButton" style="background: #004400; color: #0f0; border: 2px solid #0f0; padding: 8px 16px; font-weight: bold; cursor: pointer; margin-top: 5px; margin-left: 10px; display: none;">
|
||||
🛒 SHOP
|
||||
</button>
|
||||
<button id="animationToggleButton" style="background: #5c5c0a; color: #ff0; border: 2px solid #aa0; padding: 6px 12px; font-weight: bold; cursor: pointer; margin-top: 5px; margin-left: 10px; display: none;">
|
||||
🎬 DISABLE ANIMATION
|
||||
</button>
|
||||
@@ -71,21 +75,21 @@
|
||||
</div>
|
||||
|
||||
<!-- Payment System UI -->
|
||||
<div id="payment-ui" class="payment-section" style="display: none; background: rgba(0,50,0,0.9); border: 2px solid #0f0; padding: 10px; margin: 5px; color: #0f0;">
|
||||
<div id="payment-ui" class="payment-section" style="display: none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 2000; background: rgba(0,20,0,0.95); border: 2px solid #0f0; padding: 20px; color: #0f0; min-width: 400px; box-shadow: 0 0 20px rgba(0,255,0,0.2);">
|
||||
<div class="payment-header">
|
||||
<strong>🎮 Game Mode Selection</strong>
|
||||
<button id="close-payment" style="float: right; background: #500; color: #f00; border: 1px solid #f00; padding: 2px 6px;">✕</button>
|
||||
<button id="close-payment" style="float: right; background: #500; color: #f00; border: 1px solid #f00; padding: 2px 6px; cursor: pointer;">✕</button>
|
||||
</div>
|
||||
|
||||
<div class="game-modes" style="margin: 10px 0;">
|
||||
<div class="mode-option" data-mode="FREE" style="padding: 8px; margin: 5px 0; border: 1px solid #555; cursor: pointer;">
|
||||
<div class="game-modes" style="margin: 15px 0;">
|
||||
<div class="mode-option" data-mode="FREE" style="padding: 10px; margin: 5px 0; border: 1px solid #555; cursor: pointer; background: rgba(0,0,0,0.5);">
|
||||
<strong>🆓 FREE PLAY</strong> - No cost, no rewards
|
||||
</div>
|
||||
<div class="mode-option" data-mode="PAID_SINGLE" style="padding: 8px; margin: 5px 0; border: 1px solid #555; cursor: pointer;">
|
||||
<strong>💰 PAID SINGLE</strong> - 0.005 XMR per game, 2x payout for escape, 3x with treasure
|
||||
<div class="mode-option" data-mode="PAID_SINGLE" style="padding: 10px; margin: 5px 0; border: 1px solid #555; cursor: pointer; background: rgba(0,0,0,0.5);">
|
||||
<strong>💰 PAID SINGLE</strong> - 0.005 WOW per game, 2x payout for escape, 3x with treasure
|
||||
</div>
|
||||
<div class="mode-option" data-mode="PAID_CREDITS" style="padding: 8px; margin: 5px 0; border: 1px solid #555; cursor: pointer;">
|
||||
<strong>🎫 CREDITS PACKAGE</strong> - 0.03 XMR for 10 games, no payouts
|
||||
<div class="mode-option" data-mode="PAID_CREDITS" style="padding: 10px; margin: 5px 0; border: 1px solid #555; cursor: pointer; background: rgba(0,0,0,0.5);">
|
||||
<strong>🎫 CREDITS PACKAGE</strong> - 0.03 WOW for 10 games, no payouts
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -93,15 +97,19 @@
|
||||
<strong>Credits remaining: <span id="credits-count">0</span></strong>
|
||||
</div>
|
||||
|
||||
<div id="payment-details" style="display: none; margin: 10px 0;">
|
||||
<div><strong>Payment Required:</strong></div>
|
||||
<div>Amount: <span id="payment-amount">0</span> XMR</div>
|
||||
<div>Address: <input id="payment-address" type="text" readonly style="width: 100%; background: #000; color: #0f0; border: 1px solid #0f0; padding: 5px;"></div>
|
||||
<div style="margin: 10px 0;">
|
||||
<button id="copy-address" style="background: #050; color: #0f0; border: 1px solid #0f0; padding: 5px 10px;">📋 Copy Address</button>
|
||||
<button id="check-payment" style="background: #005; color: #0f0; border: 1px solid #0f0; padding: 5px 10px;">🔄 Check Payment</button>
|
||||
<div id="payment-details" style="display: none; margin: 15px 0;">
|
||||
<div style="margin-bottom: 5px;"><strong>Payment Required:</strong></div>
|
||||
<div style="font-size: 1.2em; margin-bottom: 10px;">Amount: <span id="payment-amount" style="color: #ff0;">0</span></div>
|
||||
<div style="margin-bottom: 5px;">Address:</div>
|
||||
<input id="payment-address" type="text" readonly style="width: 100%; background: #000; color: #0f0; border: 1px solid #0f0; padding: 8px; font-family: monospace; margin-bottom: 10px;">
|
||||
<div style="margin: 10px 0; display: flex; gap: 10px;">
|
||||
<button id="copy-address" style="flex: 1; background: #050; color: #0f0; border: 1px solid #0f0; padding: 8px; cursor: pointer;">📋 Copy Address</button>
|
||||
<button id="check-payment" style="flex: 1; background: #005; color: #0f0; border: 1px solid #0f0; padding: 8px; cursor: pointer;">🔄 Check Payment</button>
|
||||
</div>
|
||||
<div id="payment-status" style="margin: 10px 0; font-weight: bold; text-align: center;"></div>
|
||||
<div style="font-size: 0.8em; color: #aaa; text-align: center; margin-top: 15px; border-top: 1px solid #333; padding-top: 10px;">
|
||||
ℹ️ You can close this window. The payment information is also available in the sidebar/chat area.
|
||||
</div>
|
||||
<div id="payment-status" style="margin: 10px 0; font-weight: bold;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Socket event handlers for the Wowngeon game
|
||||
* Socket event handlers for the Wownerogue game
|
||||
*/
|
||||
const SocketHandlers = {
|
||||
_initialized: false,
|
||||
@@ -53,6 +53,7 @@ const SocketHandlers = {
|
||||
socket.on('address_confirmed', this.onAddressConfirmed);
|
||||
socket.on('address_update_error', this.onAddressUpdateError);
|
||||
socket.on('address_prompt', this.onAddressPrompt);
|
||||
socket.on('game_mode_info', this.onGameModeInfo);
|
||||
socket.on('payment_created', this.onPaymentCreated);
|
||||
socket.on('payment_confirmed', this.onPaymentConfirmed);
|
||||
socket.on('payment_detected', this.onPaymentDetected);
|
||||
@@ -67,7 +68,7 @@ const SocketHandlers = {
|
||||
SocketHandlers._didConnect = true;
|
||||
// Include stored session token in a lightweight resume emit (if server did not get it via handshake)
|
||||
try {
|
||||
const existing = localStorage.getItem('wowngeon_token');
|
||||
const existing = localStorage.getItem('wownerogue_token');
|
||||
if (existing) {
|
||||
// If we later decide to rely solely on query param at io() creation, this is harmless redundancy.
|
||||
socket.io.opts.query = socket.io.opts.query || {};
|
||||
@@ -84,7 +85,7 @@ const SocketHandlers = {
|
||||
|
||||
onSessionToken: function(data) {
|
||||
if (data && data.token) {
|
||||
try { localStorage.setItem('wowngeon_token', data.token); } catch(e) {}
|
||||
try { localStorage.setItem('wownerogue_token', data.token); } catch(e) {}
|
||||
$('#messages').append($('<li class="status">').text('New session established. Token stored.'));
|
||||
UI.scrollChat();
|
||||
}
|
||||
@@ -92,7 +93,7 @@ const SocketHandlers = {
|
||||
|
||||
onSessionResumed: function(data) {
|
||||
if (data && data.token) {
|
||||
try { localStorage.setItem('wowngeon_token', data.token); } catch(e) {}
|
||||
try { localStorage.setItem('wownerogue_token', data.token); } catch(e) {}
|
||||
$('#messages').append($('<li class="status">').text('Session resumed.'));
|
||||
UI.scrollChat();
|
||||
}
|
||||
@@ -301,6 +302,18 @@ const SocketHandlers = {
|
||||
|
||||
onAddressConfirmed: function(data) {
|
||||
console.log('Address confirmed:', data);
|
||||
|
||||
// Cleanup previous address-related messages to reduce clutter
|
||||
$('#messages li').each(function() {
|
||||
const text = $(this).text();
|
||||
if (text.includes('Paste your payout address') ||
|
||||
text.includes('Detected payout address') ||
|
||||
text.includes('Type \'confirm\' to save') ||
|
||||
text.includes('Address detected. Type confirm')) {
|
||||
$(this).remove();
|
||||
}
|
||||
});
|
||||
|
||||
$('#messages').append($('<li class="address-confirmed" style="color: #0f0; white-space: pre-line;">').text(data.message));
|
||||
UI.scrollChat();
|
||||
if (typeof AddressModal !== 'undefined') {
|
||||
@@ -323,9 +336,32 @@ const SocketHandlers = {
|
||||
}
|
||||
},
|
||||
|
||||
onGameModeInfo: function(data) {
|
||||
console.log('Game mode info received:', data);
|
||||
|
||||
if (typeof UI !== 'undefined' && UI.updateGameTitle) {
|
||||
UI.updateGameTitle(data.cryptoType);
|
||||
}
|
||||
|
||||
if (typeof PaymentUI !== 'undefined') {
|
||||
PaymentUI.updateConfig(data);
|
||||
// Show shop button if payments enabled
|
||||
if (data.paymentsEnabled) {
|
||||
$('#shopButton').show();
|
||||
} else {
|
||||
$('#shopButton').hide();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onPaymentCreated: function(data) {
|
||||
console.log('Payment created:', data);
|
||||
if (typeof AudioAlerts !== 'undefined') { AudioAlerts.playRequestCoin(); }
|
||||
|
||||
if (typeof PaymentUI !== 'undefined') {
|
||||
PaymentUI.showPaymentRequest(data);
|
||||
}
|
||||
|
||||
const parts = [];
|
||||
const reusedCopy = data.reused
|
||||
? '🔁 <strong>Pending payment request still active.</strong> Use the same amount and address below.'
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* Payment UI Manager
|
||||
* Handles the game mode selection and payment modal interactions
|
||||
*/
|
||||
const PaymentUI = {
|
||||
config: null,
|
||||
|
||||
init: function() {
|
||||
console.log("Initializing PaymentUI...");
|
||||
|
||||
// Bind close button
|
||||
$('#close-payment').on('click', function() {
|
||||
$('#payment-ui').hide();
|
||||
});
|
||||
|
||||
// Bind mode options
|
||||
$('.mode-option').on('click', function() {
|
||||
const mode = $(this).data('mode');
|
||||
PaymentUI.handleModeSelection(mode);
|
||||
});
|
||||
|
||||
// Bind check payment button
|
||||
$('#check-payment').on('click', function() {
|
||||
const paymentId = $(this).data('payment-id');
|
||||
if (paymentId && window.socket) {
|
||||
window.socket.emit('check_payment_status', { paymentId });
|
||||
$(this).text('Checking...');
|
||||
setTimeout(() => $('#check-payment').text('🔄 Check Payment'), 1000);
|
||||
}
|
||||
});
|
||||
|
||||
// Bind copy button
|
||||
$('#copy-address').on('click', function() {
|
||||
const addr = $('#payment-address').val();
|
||||
if(addr) {
|
||||
// Try modern clipboard API first
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(addr).then(() => {
|
||||
$('#copy-address').text('Copied!');
|
||||
setTimeout(() => $('#copy-address').html('📋 Copy Address'), 2000);
|
||||
});
|
||||
} else {
|
||||
// Fallback
|
||||
const $temp = $("<input>");
|
||||
$("body").append($temp);
|
||||
$temp.val(addr).select();
|
||||
document.execCommand("copy");
|
||||
$temp.remove();
|
||||
$('#copy-address').text('Copied!');
|
||||
setTimeout(() => $('#copy-address').html('📋 Copy Address'), 2000);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Bind Shop Button (will be added to index.html)
|
||||
$('#shopButton').on('click', function() {
|
||||
PaymentUI.show();
|
||||
});
|
||||
},
|
||||
|
||||
updateConfig: function(config) {
|
||||
console.log("PaymentUI config updated:", config);
|
||||
this.config = config;
|
||||
this.render();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (!this.config) return;
|
||||
|
||||
const currency = this.config.cryptoType || 'WOW';
|
||||
const decimals = currency === 'XMR' ? 12 : 11;
|
||||
const divisor = Math.pow(10, decimals);
|
||||
|
||||
// Update PAID_SINGLE
|
||||
if (this.config.directModeEnabled) {
|
||||
const price = (this.config.singleGamePrice / divisor).toFixed(decimals === 12 ? 4 : 2);
|
||||
const $el = $('.mode-option[data-mode="PAID_SINGLE"]');
|
||||
$el.html(`<strong>💰 PAID SINGLE</strong> - ${price} ${currency} per game<br><span style="font-size:0.8em;color:#aaa;">Payouts enabled for winners</span>`);
|
||||
$el.show();
|
||||
} else {
|
||||
$('.mode-option[data-mode="PAID_SINGLE"]').hide();
|
||||
}
|
||||
|
||||
// Update PAID_CREDITS
|
||||
if (this.config.creditsModeEnabled) {
|
||||
// Note: config.creditsPackagePrice is for the default package
|
||||
const price = (this.config.creditsPackagePrice / divisor).toFixed(decimals === 12 ? 4 : 2);
|
||||
// We assume the default package size is 10 if not specified, but let's be vague if unsure
|
||||
// Ideally we'd get package info. For now, "Credits Package" is safe.
|
||||
const $el = $('.mode-option[data-mode="PAID_CREDITS"]');
|
||||
$el.html(`<strong>🎫 CREDITS PACKAGE</strong> - ${price} ${currency}<br><span style="font-size:0.8em;color:#aaa;">Bulk discount, no payouts</span>`);
|
||||
$el.show();
|
||||
} else {
|
||||
$('.mode-option[data-mode="PAID_CREDITS"]').hide();
|
||||
}
|
||||
|
||||
// Update FREE
|
||||
if (!this.config.paymentsEnabled) {
|
||||
$('.mode-option[data-mode="FREE"]').show();
|
||||
// If payments are disabled, maybe hide the others?
|
||||
// The logic above handles hiding if enabled flags are false.
|
||||
} else {
|
||||
// If payments are enabled, FREE is usually disabled/hidden
|
||||
$('.mode-option[data-mode="FREE"]').hide();
|
||||
}
|
||||
},
|
||||
|
||||
handleModeSelection: function(mode) {
|
||||
console.log("Mode selected:", mode);
|
||||
|
||||
if (mode === 'FREE') {
|
||||
$('#payment-ui').hide();
|
||||
return;
|
||||
}
|
||||
|
||||
let paymentType = 'single_game';
|
||||
if (mode === 'PAID_CREDITS') {
|
||||
paymentType = 'credits_package';
|
||||
}
|
||||
|
||||
if (window.socket) {
|
||||
window.socket.emit('request_payment', { type: paymentType });
|
||||
|
||||
// Show loading state
|
||||
$('.game-modes').hide();
|
||||
$('#payment-details').show();
|
||||
$('#payment-status').text('Requesting payment address...');
|
||||
$('#payment-amount').text('...');
|
||||
$('#payment-address').val('Loading...');
|
||||
}
|
||||
},
|
||||
|
||||
showPaymentRequest: function(data) {
|
||||
$('#payment-ui').show();
|
||||
$('#payment-details').show();
|
||||
$('.game-modes').hide(); // Hide selection, show payment
|
||||
$('.payment-header strong').text('💳 Payment Required');
|
||||
|
||||
const displayAmount = data.humanAmount || data.amountFormatted || data.amount;
|
||||
$('#payment-amount').text(displayAmount + ' ' + (data.currency || ''));
|
||||
$('#payment-address').val(data.address);
|
||||
$('#check-payment').data('payment-id', data.id);
|
||||
|
||||
if (data.reused) {
|
||||
$('#payment-status').html('<span style="color:#ff0">♻️ Using existing pending payment</span>');
|
||||
} else {
|
||||
$('#payment-status').text('Waiting for payment...');
|
||||
}
|
||||
},
|
||||
|
||||
show: function() {
|
||||
$('#payment-ui').show();
|
||||
$('.game-modes').show();
|
||||
$('#payment-details').hide();
|
||||
$('.payment-header strong').text('🎮 Game Mode Selection');
|
||||
}
|
||||
};
|
||||
|
||||
// Expose globally
|
||||
window.PaymentUI = PaymentUI;
|
||||
|
||||
// Initialize when DOM is ready
|
||||
$(document).ready(function() {
|
||||
PaymentUI.init();
|
||||
});
|
||||
+20
-2
@@ -33,7 +33,7 @@ const UI = {
|
||||
// Initialize teletype animation
|
||||
initTeletype: function() {
|
||||
$('#banner').teletype({
|
||||
text: ["Welcome traveler...", "You have arrived at...", "The Wowngeon"],
|
||||
text: ["Welcome traveler...", "You have arrived at...", "The Dungeon"],
|
||||
typeDelay: 100,
|
||||
backDelay: 50,
|
||||
pause: 1500
|
||||
@@ -42,11 +42,29 @@ const UI = {
|
||||
// After animations are complete, set static text
|
||||
setTimeout(function() {
|
||||
$('#banner, #bannercursor').fadeOut(500, function() {
|
||||
$('#staticbanner').text("WOWGUE - A Wownero Roguelike").fadeIn(500);
|
||||
const title = UI._gameTitleFull || "WOWNEROGUE - A Wownero Roguelike";
|
||||
$('#staticbanner').text(title).fadeIn(500);
|
||||
});
|
||||
}, 14000);
|
||||
},
|
||||
|
||||
_gameTitleFull: "WOWNEROGUE - A Wownero Roguelike",
|
||||
|
||||
updateGameTitle: function(cryptoType) {
|
||||
if (cryptoType === 'XMR') {
|
||||
document.title = "XMROGUE - A Monero Roguelike";
|
||||
this._gameTitleFull = "XMROGUE - A Monero Roguelike";
|
||||
} else {
|
||||
document.title = "Wownerogue - A Wownero Roguelike";
|
||||
this._gameTitleFull = "WOWNEROGUE - A Wownero Roguelike";
|
||||
}
|
||||
|
||||
// If the static banner is already visible (animation finished), update it immediately
|
||||
if ($('#staticbanner').is(':visible') && !$('#banner').is(':visible')) {
|
||||
$('#staticbanner').text(this._gameTitleFull);
|
||||
}
|
||||
},
|
||||
|
||||
// Initialize browser detection
|
||||
initBrowserDetection: function() {
|
||||
setTimeout(function() {
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
# Wowngeon Server Configuration
|
||||
# Wownerogue Server Configuration
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Unified Payment System (recommended configuration)
|
||||
@@ -71,7 +71,7 @@ RPC_POLL_INTERVAL=2000
|
||||
# Database Configuration
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_NAME=wowgue
|
||||
DB_NAME=wownerogue
|
||||
DB_USER=jw
|
||||
DB_PASSWORD=jw
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ class DatabaseManager {
|
||||
this.pool = new Pool({
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
port: process.env.DB_PORT || 5432,
|
||||
database: process.env.DB_NAME || 'wowgue',
|
||||
database: process.env.DB_NAME || 'wownerogue',
|
||||
user: process.env.DB_USER || 'jw',
|
||||
password: process.env.DB_PASSWORD || 'jw',
|
||||
max: 20, // Maximum number of clients in the pool
|
||||
@@ -97,9 +97,6 @@ class DatabaseManager {
|
||||
this.connected = true;
|
||||
console.log('✅ Database connected successfully');
|
||||
|
||||
// Run migrations
|
||||
await this.runMigrations();
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('❌ Database connection failed:', error.message);
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ const CONSOLE_LOGGING = process.env.NODE_ENV === 'debug' || process.env.NODE_ENV
|
||||
const client = new Client({
|
||||
user: 'postgres',
|
||||
host: 'localhost',
|
||||
database: 'bitdungeon',
|
||||
database: 'wownerogue',
|
||||
password: 'postgres',
|
||||
port: 5432,
|
||||
});
|
||||
|
||||
+1
-2
@@ -3,7 +3,6 @@ const path = require('path');
|
||||
var app = express();
|
||||
var http = require('http').Server(app);
|
||||
var io = require('socket.io')(http);
|
||||
var db = require('./db/dbcalls.js');
|
||||
|
||||
// Load environment configuration
|
||||
require('dotenv').config();
|
||||
@@ -210,7 +209,7 @@ async function startServer() {
|
||||
|
||||
// Start HTTP server
|
||||
http.listen(3000, function() {
|
||||
console.log('🚀 Wowngeon server listening on *:3000');
|
||||
console.log('🚀 Wownerogue server listening on *:3000');
|
||||
console.log(`🐛 Debug mode: ${debugManager.getDebugStatus().debugMode ? 'ENABLED' : 'DISABLED'}`);
|
||||
console.log(`💰 Payment system: ${paymentSystemReady ? 'ENABLED' : 'FREE MODE ONLY'}`);
|
||||
const summary = paymentConfigManager.summarize();
|
||||
|
||||
@@ -94,7 +94,7 @@ class AddressManager {
|
||||
if (this.gameModeManager) {
|
||||
try {
|
||||
await this.gameModeManager.setUserPayoutAddress(socketId, pending);
|
||||
this.broadcastManager?.sendStatusUpdate(socketId, 'success', 'Payout address saved.');
|
||||
// this.broadcastManager?.sendStatusUpdate(socketId, 'success', 'Payout address saved.'); // Removed duplicate
|
||||
this.io.to(socketId).emit('address_confirmed', { address: pending, message: 'Payout address saved.' });
|
||||
} catch (e) {
|
||||
const normalized = normalizeError?.(e, 'Failed to save address') || e;
|
||||
@@ -105,7 +105,7 @@ class AddressManager {
|
||||
}
|
||||
} else {
|
||||
this.pending.delete(socketId);
|
||||
this.broadcastManager?.sendStatusUpdate(socketId, 'success', 'Payout address accepted (session only).');
|
||||
// this.broadcastManager?.sendStatusUpdate(socketId, 'success', 'Payout address accepted (session only).'); // Removed duplicate
|
||||
this.io.to(socketId).emit('address_confirmed', { address: pending, message: 'Payout address accepted (session only).' });
|
||||
}
|
||||
if (this.onConfirmed) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Broadcast Manager Module
|
||||
* Handles all socket.io event broadcasting for the Wowngeon game server
|
||||
* Handles all socket.io event broadcasting for the Wownerogue game server
|
||||
* Supports current player-specific events and future spectator broadcasting
|
||||
*/
|
||||
|
||||
|
||||
@@ -241,7 +241,7 @@ class ConnectionHandler {
|
||||
this.io.to(socket.id).emit('blockheight', { blockHeight: currentBlock });
|
||||
|
||||
// Send connection status
|
||||
this.broadcastManager.sendStatusUpdate(socket.id, 'connection', 'Connected to Wowngeon server');
|
||||
this.broadcastManager.sendStatusUpdate(socket.id, 'connection', 'Connected to Wownerogue server');
|
||||
}
|
||||
|
||||
_cleanupExistingMappings(socketId, clientId) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Socket Event Handlers Module
|
||||
* Handles all socket.io event processing for the Wowngeon game server
|
||||
* Handles all socket.io event processing for the Wownerogue game server
|
||||
*/
|
||||
|
||||
const user = require('../db/user');
|
||||
@@ -313,6 +313,11 @@ class SocketHandlers {
|
||||
const connectionResult = await this.connectionHandler.handleConnection(socket);
|
||||
if (!connectionResult) return; // Connection was rejected or failed
|
||||
|
||||
// Send game mode info to client
|
||||
if (this.gameModeManager) {
|
||||
socket.emit('game_mode_info', this.gameModeManager.getGameModeInfo());
|
||||
}
|
||||
|
||||
// Register event handlers
|
||||
socket.on('chat message', (msg) => this.chatHandler.handleChatMessage(socket, msg, {
|
||||
handleGameQueue: (socket) => this.queueHandler.handleGameQueue(socket, (socketId) => this.connectionHandler.getUserBySocket(socketId)),
|
||||
|
||||
+5
-2
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "bitrogue",
|
||||
"name": "wownerogue",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
@@ -7,7 +7,10 @@
|
||||
"start": "NODE_ENV=production node index.js",
|
||||
"dev": "NODE_ENV=development node index.js",
|
||||
"debug": "NODE_ENV=debug node index.js",
|
||||
"test": "jest --runInBand"
|
||||
"test": "jest --runInBand",
|
||||
"db:create": "node scripts/setup_db.js",
|
||||
"db:drop": "node scripts/setup_db.js drop",
|
||||
"db:reset": "npm run db:drop && npm run db:create"
|
||||
},
|
||||
"config": {
|
||||
"debug": {
|
||||
|
||||
@@ -51,7 +51,7 @@ class MoneroPayService {
|
||||
|
||||
const response = await axios.post(`${this.endpoint}/receive`, {
|
||||
amount: parseInt(amount),
|
||||
description: description || `Wowgue ${paymentType}`,
|
||||
description: description || `Wownerogue ${paymentType}`,
|
||||
callback_url: callbackUrl
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
require('dotenv').config({ path: '../.env' });
|
||||
const { Client } = require('pg');
|
||||
|
||||
const DB_CONFIG = {
|
||||
user: process.env.DB_USER || 'jw',
|
||||
password: process.env.DB_PASSWORD || 'jw',
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
port: process.env.DB_PORT || 5432,
|
||||
database: process.env.DB_NAME || 'wownerogue'
|
||||
};
|
||||
|
||||
// Admin config to create user/db
|
||||
const ADMIN_CONFIG = {
|
||||
user: process.env.DB_ADMIN_USER || 'postgres',
|
||||
password: process.env.DB_ADMIN_PASSWORD || 'postgres', // Try default
|
||||
host: DB_CONFIG.host,
|
||||
port: DB_CONFIG.port,
|
||||
database: 'postgres'
|
||||
};
|
||||
|
||||
async function setup() {
|
||||
console.log('🔧 Starting database setup...');
|
||||
|
||||
// Try to connect as admin
|
||||
const client = new Client(ADMIN_CONFIG);
|
||||
|
||||
try {
|
||||
await client.connect();
|
||||
console.log('✅ Connected as admin');
|
||||
} catch (err) {
|
||||
console.log('⚠️ Could not connect as admin (postgres/postgres). Trying with current user...');
|
||||
// Fallback: try connecting with current user to 'postgres' db
|
||||
client.end();
|
||||
const fallbackClient = new Client({
|
||||
...ADMIN_CONFIG,
|
||||
user: process.env.USER || 'postgres',
|
||||
password: '', // often passwordless for local socket
|
||||
});
|
||||
try {
|
||||
await fallbackClient.connect();
|
||||
console.log('✅ Connected as ' + (process.env.USER || 'postgres'));
|
||||
// Swap client
|
||||
Object.assign(client, fallbackClient);
|
||||
} catch (e) {
|
||||
console.error('❌ Could not connect to postgres instance. Please ensure PostgreSQL is running and you have rights to create databases.');
|
||||
console.error('Error:', err.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. Create User if not exists
|
||||
const userRes = await client.query(`SELECT 1 FROM pg_roles WHERE rolname=$1`, [DB_CONFIG.user]);
|
||||
if (userRes.rowCount === 0) {
|
||||
console.log(`👤 Creating user ${DB_CONFIG.user}...`);
|
||||
await client.query(`CREATE USER "${DB_CONFIG.user}" WITH PASSWORD '${DB_CONFIG.password}' CREATEDB`);
|
||||
} else {
|
||||
console.log(`👤 User ${DB_CONFIG.user} already exists`);
|
||||
}
|
||||
|
||||
// 2. Create Database if not exists
|
||||
const dbRes = await client.query(`SELECT 1 FROM pg_database WHERE datname=$1`, [DB_CONFIG.database]);
|
||||
if (dbRes.rowCount === 0) {
|
||||
console.log(`📦 Creating database ${DB_CONFIG.database}...`);
|
||||
await client.query(`CREATE DATABASE "${DB_CONFIG.database}" OWNER "${DB_CONFIG.user}"`);
|
||||
} else {
|
||||
console.log(`📦 Database ${DB_CONFIG.database} already exists`);
|
||||
}
|
||||
|
||||
console.log('✅ Setup complete!');
|
||||
} catch (err) {
|
||||
console.error('❌ Setup failed:', err.message);
|
||||
} finally {
|
||||
await client.end();
|
||||
}
|
||||
}
|
||||
|
||||
async function drop() {
|
||||
console.log('🔥 Dropping database...');
|
||||
const client = new Client(ADMIN_CONFIG);
|
||||
try {
|
||||
await client.connect();
|
||||
// Terminate connections first
|
||||
await client.query(`
|
||||
SELECT pg_terminate_backend(pid)
|
||||
FROM pg_stat_activity
|
||||
WHERE datname = $1
|
||||
`, [DB_CONFIG.database]);
|
||||
|
||||
await client.query(`DROP DATABASE IF EXISTS "${DB_CONFIG.database}"`);
|
||||
console.log(`✅ Database ${DB_CONFIG.database} dropped`);
|
||||
} catch (err) {
|
||||
console.error('❌ Drop failed:', err.message);
|
||||
} finally {
|
||||
await client.end();
|
||||
}
|
||||
}
|
||||
|
||||
const action = process.argv[2];
|
||||
if (action === 'drop') {
|
||||
drop();
|
||||
} else {
|
||||
setup();
|
||||
}
|
||||
@@ -1,418 +0,0 @@
|
||||
After a deep review of your Wowngeon codebase, I'll provide comprehensive feedback on production readiness and architectural improvements.
|
||||
|
||||
## 🔍 Deep Code Review Summary
|
||||
|
||||
### Critical Production Issues
|
||||
|
||||
#### 1. **Security Vulnerabilities**
|
||||
```javascript
|
||||
// src/network/socketHandlers.js - SQL Injection Risk
|
||||
const query = `SELECT * FROM users WHERE socket_id = '${socketId}'`; // DANGEROUS
|
||||
```
|
||||
**Fix**: Always use parameterized queries (you mostly do, but found a few raw queries)
|
||||
|
||||
#### 2. **Missing Rate Limiting**
|
||||
The chat system has basic cooldown but lacks proper rate limiting for:
|
||||
- Payment request creation
|
||||
- Address confirmations
|
||||
- Game entry attempts
|
||||
- WebSocket connections per IP
|
||||
|
||||
#### 3. **Insufficient Input Validation**
|
||||
```javascript
|
||||
// src/network/addressManager.js
|
||||
detectAddress(message) {
|
||||
// Only checks regex, no length validation or checksum verification
|
||||
const xmrMatch = message.match(/\b[48AB][0-9A-Za-z]{94}\b/);
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. **Memory Leaks**
|
||||
- Movement manager's `_postMoveHooks` Map grows unbounded
|
||||
- Session manager doesn't clean up expired tokens
|
||||
- Payment handlers' confirmation tracking never expires old entries
|
||||
|
||||
### Architecture Improvements
|
||||
|
||||
#### 1. **Merge PAID_SINGLE and PAID_CREDITS**
|
||||
Yes, this is absolutely feasible and recommended:
|
||||
|
||||
```javascript
|
||||
// Proposed unified structure
|
||||
class UnifiedPaymentMode {
|
||||
constructor(config) {
|
||||
this.modes = {
|
||||
PAY_PER_GAME: {
|
||||
enabled: config.PAY_PER_GAME_ENABLED,
|
||||
price: config.GAME_PRICE,
|
||||
payoutMultipliers: { escape: 2, treasure: 3 }
|
||||
},
|
||||
CREDITS: {
|
||||
enabled: config.CREDITS_ENABLED,
|
||||
packageSize: config.CREDITS_PACKAGE_SIZE,
|
||||
packagePrice: config.CREDITS_PACKAGE_PRICE,
|
||||
payoutsEnabled: config.CREDITS_PAYOUT_ENABLED
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async processPayment(user, preferredMode) {
|
||||
// Unified payment flow
|
||||
if (preferredMode === 'CREDITS' && this.modes.CREDITS.enabled) {
|
||||
return this.purchaseCredits(user);
|
||||
} else if (this.modes.PAY_PER_GAME.enabled) {
|
||||
return this.payPerGame(user);
|
||||
}
|
||||
throw new Error('No payment mode available');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. **Database Schema Issues**
|
||||
```sql
|
||||
-- Missing indexes on frequently queried columns
|
||||
CREATE INDEX idx_games_user_id ON games(user_id);
|
||||
CREATE INDEX idx_payments_status ON payments(status);
|
||||
CREATE INDEX idx_users_anon_token ON users(anon_token); -- Already added, good!
|
||||
|
||||
-- Missing foreign key constraints
|
||||
ALTER TABLE games ADD CONSTRAINT fk_games_user
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
```
|
||||
|
||||
#### 3. **Error Handling Gaps**
|
||||
```javascript
|
||||
// Many async operations lack proper error boundaries
|
||||
async handleGameStart(socket) {
|
||||
try {
|
||||
// ... game start logic
|
||||
} catch (error) {
|
||||
console.error('Game start failed:', error);
|
||||
// Missing: User notification, rollback, metrics
|
||||
socket.emit('error', { message: 'Game start failed' });
|
||||
this.metrics.recordError('game_start', error);
|
||||
await this.rollbackGameStart(socket.id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Production Readiness Checklist
|
||||
|
||||
#### ❌ **Critical - Must Fix**
|
||||
- [ ] SQL injection vulnerabilities
|
||||
- [ ] Rate limiting implementation
|
||||
- [ ] Memory leak fixes
|
||||
- [ ] Error recovery & rollback logic
|
||||
- [ ] Payment confirmation idempotency
|
||||
|
||||
#### ⚠️ **Important - Should Fix**
|
||||
- [ ] Unified PAID mode architecture
|
||||
- [ ] WebSocket authentication tokens
|
||||
- [ ] Database transaction boundaries
|
||||
- [ ] Comprehensive logging (not just console.log)
|
||||
- [ ] Health check endpoints
|
||||
- [ ] Graceful shutdown handling
|
||||
|
||||
#### 💡 **Nice to Have**
|
||||
- [ ] Metrics collection (Prometheus/StatsD)
|
||||
- [ ] Circuit breakers for RPC calls
|
||||
- [ ] Request tracing (correlation IDs)
|
||||
- [ ] Admin dashboard
|
||||
- [ ] A/B testing framework
|
||||
|
||||
## 🏗️ Proposed Refactoring Plan
|
||||
|
||||
### Phase 1: Security & Stability (1 week)
|
||||
```javascript
|
||||
// 1. Add comprehensive input validation
|
||||
class ValidationService {
|
||||
validateAddress(address, currency) {
|
||||
// Length, charset, checksum validation
|
||||
if (currency === 'WOW') {
|
||||
return this.validateWowneroAddress(address);
|
||||
}
|
||||
return this.validateMoneroAddress(address);
|
||||
}
|
||||
|
||||
validatePaymentAmount(amount) {
|
||||
// Ensure positive integer, within bounds
|
||||
const parsed = BigInt(amount);
|
||||
if (parsed <= 0n || parsed > MAX_PAYMENT) {
|
||||
throw new ValidationError('Invalid payment amount');
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Implement rate limiting
|
||||
class RateLimiter {
|
||||
constructor(redis) {
|
||||
this.redis = redis;
|
||||
this.limits = {
|
||||
'payment:create': { window: 60, max: 3 },
|
||||
'game:start': { window: 60, max: 10 },
|
||||
'chat:message': { window: 10, max: 5 }
|
||||
};
|
||||
}
|
||||
|
||||
async checkLimit(userId, action) {
|
||||
const key = `rate:${userId}:${action}`;
|
||||
const count = await this.redis.incr(key);
|
||||
if (count === 1) {
|
||||
await this.redis.expire(key, this.limits[action].window);
|
||||
}
|
||||
if (count > this.limits[action].max) {
|
||||
throw new RateLimitError(`Too many ${action} requests`);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 2: Unified Payment System (3-4 days)
|
||||
```javascript
|
||||
// Unified payment configuration
|
||||
const PAYMENT_CONFIG = {
|
||||
enabled: true,
|
||||
modes: {
|
||||
direct: {
|
||||
enabled: process.env.DIRECT_PAY_ENABLED === 'true',
|
||||
price: BigInt(process.env.GAME_PRICE || '1e11'),
|
||||
payouts: {
|
||||
enabled: true,
|
||||
escape: 2,
|
||||
treasure: 3
|
||||
}
|
||||
},
|
||||
credits: {
|
||||
enabled: process.env.CREDITS_ENABLED === 'true',
|
||||
packages: [
|
||||
{ credits: 10, price: BigInt('5e11') },
|
||||
{ credits: 25, price: BigInt('10e11') },
|
||||
{ credits: 100, price: BigInt('35e11') }
|
||||
],
|
||||
payouts: {
|
||||
enabled: process.env.CREDITS_PAYOUT_ENABLED === 'true'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Phase 3: Monitoring & Observability (2-3 days)
|
||||
```javascript
|
||||
// Add comprehensive metrics
|
||||
class MetricsCollector {
|
||||
constructor() {
|
||||
this.metrics = {
|
||||
payments: new prometheus.Counter({
|
||||
name: 'payments_total',
|
||||
help: 'Total payments processed',
|
||||
labelNames: ['status', 'mode']
|
||||
}),
|
||||
gameTime: new prometheus.Histogram({
|
||||
name: 'game_duration_seconds',
|
||||
help: 'Game duration in seconds',
|
||||
buckets: [30, 60, 120, 300, 600]
|
||||
}),
|
||||
activeGames: new prometheus.Gauge({
|
||||
name: 'active_games',
|
||||
help: 'Currently active games'
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Add structured logging
|
||||
const winston = require('winston');
|
||||
const logger = winston.createLogger({
|
||||
format: winston.format.json(),
|
||||
transports: [
|
||||
new winston.transports.File({ filename: 'error.log', level: 'error' }),
|
||||
new winston.transports.File({ filename: 'combined.log' })
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
## 📊 Database Improvements
|
||||
|
||||
```sql
|
||||
-- Add missing indexes
|
||||
CREATE INDEX CONCURRENTLY idx_payments_created_at ON payments(created_at);
|
||||
CREATE INDEX CONCURRENTLY idx_games_status_end ON games(status, ended_at);
|
||||
CREATE INDEX CONCURRENTLY idx_users_last_seen ON users(last_seen);
|
||||
|
||||
-- Add audit table
|
||||
CREATE TABLE audit_log (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER REFERENCES users(id),
|
||||
action VARCHAR(50) NOT NULL,
|
||||
details JSONB,
|
||||
ip_address INET,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Add payment_attempts for idempotency
|
||||
CREATE TABLE payment_attempts (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id INTEGER REFERENCES users(id),
|
||||
amount BIGINT NOT NULL,
|
||||
address VARCHAR(255) NOT NULL,
|
||||
idempotency_key VARCHAR(255) UNIQUE,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
## 🔒 Security Enhancements
|
||||
|
||||
```javascript
|
||||
// Add request signing for critical operations
|
||||
class SecurityMiddleware {
|
||||
validateSignature(req, res, next) {
|
||||
const signature = req.headers['x-signature'];
|
||||
const timestamp = req.headers['x-timestamp'];
|
||||
|
||||
if (!signature || !timestamp) {
|
||||
return res.status(401).json({ error: 'Missing signature' });
|
||||
}
|
||||
|
||||
const age = Date.now() - parseInt(timestamp);
|
||||
if (age > 30000) { // 30 seconds
|
||||
return res.status(401).json({ error: 'Request expired' });
|
||||
}
|
||||
|
||||
const expected = crypto
|
||||
.createHmac('sha256', process.env.API_SECRET)
|
||||
.update(`${timestamp}:${JSON.stringify(req.body)}`)
|
||||
.digest('hex');
|
||||
|
||||
if (signature !== expected) {
|
||||
return res.status(401).json({ error: 'Invalid signature' });
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🎮 Game Logic Improvements
|
||||
|
||||
```javascript
|
||||
// Add deterministic game generation for replays/verification
|
||||
class DeterministicGameGenerator {
|
||||
constructor(seed) {
|
||||
this.rng = new ROT.RNG();
|
||||
this.rng.setSeed(seed);
|
||||
}
|
||||
|
||||
generateDungeon(config) {
|
||||
// Use seeded RNG for all random operations
|
||||
const width = config.width;
|
||||
const height = config.height;
|
||||
const map = new ROT.Map.Digger(width, height, {
|
||||
roomWidth: [3, 9],
|
||||
roomHeight: [3, 5],
|
||||
corridorLength: [3, 10],
|
||||
dugPercentage: 0.2,
|
||||
timeLimit: 1000
|
||||
});
|
||||
|
||||
// Store seed for replay/verification
|
||||
return {
|
||||
map: map.create(),
|
||||
seed: this.rng.getSeed(),
|
||||
checksum: this.calculateChecksum(map)
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🚀 Performance Optimizations
|
||||
|
||||
```javascript
|
||||
// Add caching layer
|
||||
class CacheService {
|
||||
constructor(redis) {
|
||||
this.redis = redis;
|
||||
this.ttls = {
|
||||
user: 300, // 5 minutes
|
||||
gameState: 60, // 1 minute
|
||||
blockHeight: 2 // 2 seconds
|
||||
};
|
||||
}
|
||||
|
||||
async getCachedUser(userId) {
|
||||
const cached = await this.redis.get(`user:${userId}`);
|
||||
if (cached) return JSON.parse(cached);
|
||||
|
||||
const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
|
||||
await this.redis.setex(
|
||||
`user:${userId}`,
|
||||
this.ttls.user,
|
||||
JSON.stringify(user)
|
||||
);
|
||||
return user;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 📝 Testing Improvements
|
||||
|
||||
```javascript
|
||||
// Add integration tests
|
||||
describe('Payment Flow Integration', () => {
|
||||
let server, client, mockWallet;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockWallet = new MockWalletService();
|
||||
server = await createServer({ wallet: mockWallet });
|
||||
client = io(`http://localhost:${server.port}`);
|
||||
});
|
||||
|
||||
test('complete payment flow', async () => {
|
||||
// 1. Connect and get session
|
||||
await client.connect();
|
||||
const session = await waitForEvent(client, 'session_token');
|
||||
|
||||
// 2. Set payout address
|
||||
client.emit('chat_message', 'WW1234...');
|
||||
await waitForEvent(client, 'address_detected');
|
||||
client.emit('chat_message', 'confirm');
|
||||
await waitForEvent(client, 'address_confirmed');
|
||||
|
||||
// 3. Start game
|
||||
client.emit('auto_start');
|
||||
const payment = await waitForEvent(client, 'payment_created');
|
||||
|
||||
// 4. Simulate payment
|
||||
await mockWallet.simulatePayment(payment.address, payment.amount);
|
||||
await waitForEvent(client, 'payment_detected');
|
||||
|
||||
// 5. Confirm payment
|
||||
await mockWallet.confirmPayment();
|
||||
await waitForEvent(client, 'payment_confirmed');
|
||||
await waitForEvent(client, 'game_start');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## 🎯 Immediate Action Items
|
||||
|
||||
1. **Fix SQL injection vulnerabilities** (2 hours)
|
||||
2. **Add rate limiting** (4 hours)
|
||||
3. **Fix memory leaks** (3 hours)
|
||||
4. **Add database migrations for indexes** (1 hour)
|
||||
5. **Implement unified payment mode** (8 hours)
|
||||
6. **Add comprehensive error handling** (6 hours)
|
||||
7. **Set up structured logging** (2 hours)
|
||||
8. **Add health check endpoints** (1 hour)
|
||||
|
||||
## 📈 Long-term Roadmap
|
||||
|
||||
1. **Month 1**: Security, stability, unified payments
|
||||
2. **Month 2**: Monitoring, admin dashboard, metrics
|
||||
3. **Month 3**: Scaling (Redis cache, load balancing)
|
||||
4. **Month 4**: Advanced features (tournaments, leaderboards)
|
||||
|
||||
The codebase shows good architectural foundations but needs these production hardening steps. The unified PAID mode is definitely the right direction—it will simplify both code and user experience.
|
||||
|
||||
Would you like me to start implementing any of these improvements immediately? I'd recommend beginning with the security fixes and unified payment mode.
|
||||
Reference in New Issue
Block a user