add db commands and test payout (worked!)

This commit is contained in:
jwinterm
2025-11-23 13:33:04 -05:00
parent fa61d60bc0
commit deca4a4685
20 changed files with 403 additions and 1277 deletions
+10 -6
View File
@@ -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
+17
View File
@@ -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).
-812
View File
@@ -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
View File
@@ -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>
+40 -4
View File
@@ -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.'
+165
View File
@@ -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
View File
@@ -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
View File
@@ -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
+1 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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();
+2 -2
View File
@@ -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 -1
View File
@@ -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
*/
+1 -1
View File
@@ -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) {
+6 -1
View File
@@ -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
View File
@@ -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": {
+1 -1
View File
@@ -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
});
+104
View File
@@ -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();
}
-418
View File
@@ -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.