feat: network-aware currency label (sXMR), game name, and Smirk gating

- Currency display is now network-aware: XMR on a test network shows sXMR
  (stagenet) / tXMR (testnet) so it's clear the coins aren't mainnet; mainnet
  XMR shows XMR, Wownero stays WOW. A fork running mainnet automatically shows
  XMR. cryptoType (used for decimals/QR asset) is unchanged.
- Product name (Monerogue/Wownerogue) is derived from the currency and no
  longer hardcoded: the chat "Connected to <name> server" message, payment
  descriptions, and the verify page title all follow it.
- Smirk is forced off on any test network (stagenet/testnet) regardless of
  SMIRK_ENABLED, since Smirk only works on mainnet — so forks don't have to
  remember the env flag.

New shared helpers (gameNameFor, currencyLabelFor, isTestNetworkFor); exposed
via game_mode_info (currencyLabel, gameName). Full suite: 22 suites / 159 tests.
This commit is contained in:
jwinterm
2026-05-31 15:57:59 -04:00
parent 04774c4ca2
commit e6936f47d9
7 changed files with 57 additions and 17 deletions
+3 -2
View File
@@ -515,7 +515,7 @@ const SocketHandlers = {
if (won && data.payout && data.payout.amount) {
var decimals = (SocketHandlers._cryptoType === 'WOW') ? 11 : 12;
var amountFormatted = (data.payout.amount / Math.pow(10, decimals)).toFixed(4);
var currency = SocketHandlers._cryptoType || 'WOW';
var currency = SocketHandlers._currencyLabel || SocketHandlers._cryptoType || 'WOW';
var multiplierText = data.payout.multiplier ? ' (' + data.payout.multiplier + 'x)' : '';
var treasureText = data.payout.treasure ? ' + Treasure bonus!' : '';
$('#messages').append($('<li style="color:#4ade80; font-weight:bold; padding:4px 0;">').html(
@@ -608,6 +608,7 @@ const SocketHandlers = {
SocketHandlers._paymentsEnabled = !!data.paymentsEnabled;
SocketHandlers._smirkEnabled = data.smirkEnabled !== false; // Default to true if not specified
SocketHandlers._cryptoType = data.cryptoType || 'WOW';
SocketHandlers._currencyLabel = data.currencyLabel || data.cryptoType || 'WOW'; // sXMR on stagenet
SocketHandlers._explorerTxUrl = data.explorerTxUrl || null;
// Initialize Smirk auth if enabled and not already initialized
@@ -762,7 +763,7 @@ const SocketHandlers = {
: '💳 <strong>Payment Required</strong>';
parts.push(reusedCopy);
const displayAmount = data.humanAmount || data.amountFormatted || data.amount;
parts.push('Amount: ' + displayAmount + ' ' + (data.cryptoType || ''));
parts.push('Amount: ' + displayAmount + ' ' + (data.currencyLabel || SocketHandlers._currencyLabel || data.cryptoType || ''));
const shortAddr = data.address.substring(0, 10) + '…' + data.address.slice(-6);
parts.push('Address: <span class="pay-address-full" style="cursor:pointer;" title="Click to toggle full address">' + shortAddr + '</span>' +
' <button class="copy-pay-address" data-address="' + data.address + '" style="margin-left:4px;padding:1px 4px;font-size:11px;cursor:pointer;">Copy</button>');
+3 -1
View File
@@ -104,7 +104,9 @@ const PaymentUI = {
render: function() {
if (!this.config) return;
const currency = this.config.cryptoType || 'WOW';
// Display label (sXMR on stagenet) — distinct from cryptoType, which formatPrice
// still uses for decimals.
const currency = this.config.currencyLabel || this.config.cryptoType || 'WOW';
const hasCredits = this.userCredits >= (this.config.creditsPerGame || 1);
const creditPackages = this.config.creditPackages || [];
+15 -5
View File
@@ -9,7 +9,9 @@ const {
inferCurrencyDecimals: inferCurrencyDecimalsForSymbol,
formatAtomic,
formatAtomicHuman,
getDecimalDivisor
getDecimalDivisor,
gameNameFor,
currencyLabelFor
} = require('./helpers/gameModeUtils');
const { ValidationError, normalizeError } = require('../utils/errors');
const paymentConfig = require('../config/paymentConfig');
@@ -68,6 +70,11 @@ class GameModeManager {
return inferCurrencyDecimalsForSymbol(symbol);
}
// Product name (Monerogue/Wownerogue) and network-aware currency label (sXMR on
// stagenet, XMR on mainnet, WOW for Wownero) — derived so they always track config.
get gameName() { return gameNameFor(this.cryptoType); }
get currencyLabel() { return currencyLabelFor(this.cryptoType, this.network); }
applyLegacyEnvConfig() {
this.setLegacyGameMode(process.env.GAME_MODE || 'FREE');
this.cryptoType = process.env.CRYPTO_TYPE || this.cryptoType;
@@ -904,7 +911,7 @@ class GameModeManager {
switch (paymentType) {
case 'single_game': {
amount = this.singleGamePrice;
description = `Wowngeon single game entry (${this.cryptoType})`;
description = `${this.gameName} single game entry (${this.currencyLabel})`;
break;
}
case 'credits_package': {
@@ -927,7 +934,7 @@ class GameModeManager {
const creditCount = selectedPackage?.credits ?? 10;
const bonusCredits = selectedPackage?.bonus ?? 0;
const bonusText = bonusCredits > 0 ? ` (+${bonusCredits} bonus)` : '';
description = `Wowngeon ${creditCount}${bonusText} credits package (${this.cryptoType})`;
description = `${this.gameName} ${creditCount}${bonusText} credits package (${this.currencyLabel})`;
break;
}
default:
@@ -1147,6 +1154,8 @@ class GameModeManager {
return {
mode: this.gameMode,
cryptoType: this.cryptoType,
currencyLabel: this.currencyLabel, // sXMR on stagenet, XMR on mainnet, WOW for Wownero
gameName: this.gameName, // Monerogue / Wownerogue
network: this.network,
isTestNetwork: this.isTestNetwork,
testnetWarning: testnetWarning,
@@ -1176,8 +1185,9 @@ class GameModeManager {
allowInFreeMode: this.configSnapshot?.earlyEntry?.allowInFreeMode ?? false,
allowInCreditsMode: this.configSnapshot?.earlyEntry?.allowInCreditsMode ?? false
},
// Smirk wallet integration (disabled for Monero stagenet)
smirkEnabled: process.env.SMIRK_ENABLED !== 'false',
// Smirk wallet integration. Smirk only works on mainnet, so it is forced off on
// any test network (stagenet/testnet) regardless of SMIRK_ENABLED.
smirkEnabled: process.env.SMIRK_ENABLED !== 'false' && !this.isTestNetwork,
explorerTxUrl: process.env.EXPLORER_TX_URL || null
};
}
+25 -1
View File
@@ -30,6 +30,27 @@ const getDecimalDivisor = (decimals = DEFAULT_DECIMALS) => {
return Math.pow(10, normalized);
};
// The product name for a given currency (Monero -> Monerogue, otherwise Wownerogue).
const gameNameFor = (cryptoType) => (String(cryptoType || '').toUpperCase() === 'XMR' ? 'Monerogue' : 'Wownerogue');
// The user-facing currency label, network-aware: Monero on a test network shows sXMR
// (stagenet) / tXMR (testnet) so it's clear the coins aren't mainnet; mainnet shows XMR.
// Wownero has no test networks, so it's always WOW.
const currencyLabelFor = (cryptoType, network) => {
const ct = String(cryptoType || '').toUpperCase();
const net = String(network || 'mainnet').toLowerCase();
if (ct === 'XMR' && (net === 'stagenet' || net === 'testnet')) {
return (net === 'testnet' ? 't' : 's') + 'XMR';
}
return ct;
};
// Smirk wallet only works on mainnet Monero/Wownero, so it must be off on test networks.
const isTestNetworkFor = (network) => {
const net = String(network || 'mainnet').toLowerCase();
return net === 'stagenet' || net === 'testnet';
};
const formatAtomic = ({ value, decimals, digits = 6 }) => {
if (value === undefined || value === null) {
return '0';
@@ -60,5 +81,8 @@ module.exports = {
inferCurrencyDecimals,
getDecimalDivisor,
formatAtomic,
formatAtomicHuman
formatAtomicHuman,
gameNameFor,
currencyLabelFor,
isTestNetworkFor
};
+5 -4
View File
@@ -492,9 +492,10 @@ app.get('/api/user/:socketId/payouts', asyncHandler(async (req, res) => {
// =============================================================================
// Smirk Wallet Authentication Endpoints
// =============================================================================
// Smirk is a browser extension wallet for Wownero/Monero.
// Disable for Monero stagenet deployments (Smirk doesn't support stagenet).
const smirkEnabled = process.env.SMIRK_ENABLED !== 'false';
// Smirk is a browser extension wallet for Wownero/Monero (mainnet only).
// Forced off on any test network (stagenet/testnet), regardless of SMIRK_ENABLED.
const { isTestNetworkFor } = require('./game/helpers/gameModeUtils');
const smirkEnabled = process.env.SMIRK_ENABLED !== 'false' && !isTestNetworkFor(process.env.MONERO_NETWORK);
if (smirkEnabled) {
app.use(createAuthRoutes({ db: databaseManager }));
}
@@ -640,7 +641,7 @@ app.get('/verify/:gameId', asyncHandler(async (req, res) => {
}
// Return the server-rendered HTML verification page (see src/views/verifyPage.js).
res.send(renderVerifyPage(gameId, gameRecord));
res.send(renderVerifyPage(gameId, gameRecord, gameModeManager.gameName));
}));
// API endpoint for programmatic verification
+2 -1
View File
@@ -5,6 +5,7 @@
const user = require('../db/user');
const { clientIp } = require('./rateLimitContext');
const { gameNameFor } = require('../game/helpers/gameModeUtils');
class ConnectionHandler {
constructor({ io, broadcastManager, debugManager, sessionManager, rateLimiter }) {
@@ -275,7 +276,7 @@ class ConnectionHandler {
this.io.to(socket.id).emit('blockheight', { blockHeight: currentBlock });
// Send connection status
this.broadcastManager.sendStatusUpdate(socket.id, 'connection', 'Connected to Wownerogue server');
this.broadcastManager.sendStatusUpdate(socket.id, 'connection', `Connected to ${gameNameFor(process.env.CRYPTO_TYPE)} server`);
}
_cleanupExistingMappings(socketId, clientId) {
+4 -3
View File
@@ -14,7 +14,8 @@ function esc(value) {
.replace(/'/g, '&#39;');
}
function renderVerifyPage(gameId, gameRecord) {
function renderVerifyPage(gameId, gameRecord, gameName) {
const name = gameName || 'Wownerogue';
const recordBlock = gameRecord ? `
<div class="box">
<h3>Game Record</h3>
@@ -31,7 +32,7 @@ function renderVerifyPage(gameId, gameRecord) {
<!DOCTYPE html>
<html>
<head>
<title>Wownerogue - Game Verification</title>
<title>${esc(name)} - Game Verification</title>
<style>
body { background: #0a0a0a; color: #0f0; font-family: monospace; padding: 20px; max-width: 800px; margin: 0 auto; }
h1 { color: #0ff; }
@@ -48,7 +49,7 @@ function renderVerifyPage(gameId, gameRecord) {
</style>
</head>
<body>
<h1>🔐 Wownerogue Provably Fair Verification</h1>
<h1>🔐 ${esc(name)} Provably Fair Verification</h1>
<div class="box">
<h3>How it works:</h3>