feat(wow): admit official free-only payments pause

This commit is contained in:
2026-10-21 16:09:09 -04:00
parent 7c3210d1f1
commit 86ed262628
7 changed files with 254 additions and 42 deletions
+4
View File
@@ -37,6 +37,10 @@ PAID_ACKNOWLEDGEMENT_REQUIRED=true
# RESTRICTED_LOCATIONS_NOTICE=
# Free runs rank on the Pleb board; purchased credits rank on the paid prestige board.
# To pause the official service's financial intake and credit play, explicitly set
# PAYMENTS_ENABLED=false, GAME_MODE=FREE, PAYMENT_MODES= (empty), CREDITS_ENABLED=false,
# EARLY_ENTRY_CREDITS_MODE=false, and leave both financial-event sink fields empty.
# Keep every payout and paid-match selector false. Production preflight checks the full contract.
PAYMENTS_ENABLED=true
FREE_PLAY_ENABLED=true
GAME_MODE=PAID_CREDITS
+14 -4
View File
@@ -8,7 +8,8 @@
const {
SOURCE_SOFTWARE,
getOperatedProductProfile
getOperatedProductProfile,
isWowPaymentsPaused
} = require('./operatedProductProfiles');
const DEFAULT_POLICY_VERSION = '2026-07-22';
@@ -21,6 +22,7 @@ const ACKNOWLEDGEMENT_KEYS = new Set([
'riskAccepted',
'testnetUnderstood'
]);
const WOW_PAYMENTS_PAUSED_NOTICE = 'Payments and prestige credit entry are currently unavailable. Free play remains available.';
function isTrue(value) {
return ['true', '1', 'yes', 'on'].includes(String(value || '').trim().toLowerCase());
@@ -78,12 +80,20 @@ function paidAcknowledgementRequired(env, gameModeManager) {
}
function buildCommerceDisclosure(gameModeManager, env = process.env) {
const operatedProduct = getOperatedProductProfile(env);
const profile = getOperatedProductProfile(env);
const pausedWow = isWowPaymentsPaused(env);
const operatedProduct = pausedWow
? Object.freeze({
...profile,
commerceSummary: WOW_PAYMENTS_PAUSED_NOTICE,
availabilityNotice: WOW_PAYMENTS_PAUSED_NOTICE
})
: profile;
const cryptoType = String(gameModeManager?.cryptoType || env.CRYPTO_TYPE || 'WOW').toUpperCase();
const network = String(gameModeManager?.network || env.MONERO_NETWORK || 'mainnet').toLowerCase();
const isTestNetwork = Boolean(gameModeManager?.isTestNetwork)
|| (cryptoType === 'XMR' && network !== 'mainnet');
const paymentsEnabled = Boolean(gameModeManager?.paymentsEnabled);
const paymentsEnabled = !pausedWow && Boolean(gameModeManager?.paymentsEnabled);
const directEnabled = paymentsEnabled && Boolean(gameModeManager?.directModeEnabled);
const creditsEnabled = paymentsEnabled && Boolean(gameModeManager?.creditsModeEnabled);
// A stale subordinate payout flag for a disabled product must not be advertised as an active
@@ -92,7 +102,7 @@ function buildCommerceDisclosure(gameModeManager, env = process.env) {
&& Boolean(gameModeManager?.isPayoutEnabledForMode?.('PAID_SINGLE'));
const creditsPayouts = creditsEnabled
&& Boolean(gameModeManager?.isPayoutEnabledForMode?.('PAID_CREDITS'));
const cryptoMatchPayouts = isTrue(env.MATCH_ENABLED)
const cryptoMatchPayouts = !pausedWow && isTrue(env.MATCH_ENABLED)
&& isTrue(env.MATCH_CRYPTO_RACE_ENABLED)
&& isTrue(env.MATCH_PAYOUTS_ENABLED)
&& Boolean(gameModeManager?.payoutsEnabled);
+34 -8
View File
@@ -75,12 +75,19 @@ function getOperatedProductProfile(env = process.env) {
return PROFILES[selectedProfileId(env)] || null;
}
function isWowPaymentsPaused(env = process.env) {
return selectedProfileId(env) === OPERATED_PRODUCT_PROFILE_IDS.WOW_PRESTIGE
&& isExplicitFalse(env.PAYMENTS_ENABLED);
}
function selectPublicPaidModeDescriptors(env = process.env, descriptors = {}) {
const profile = getOperatedProductProfile(env);
if (!profile) {
return { ...descriptors };
}
if (isWowPaymentsPaused(env)) return {};
const descriptor = descriptors[profile.gameMode];
return descriptor === undefined ? {} : { [profile.gameMode]: descriptor };
}
@@ -144,10 +151,13 @@ function validateOperatedProductProfile(env = process.env, config = {}) {
`LEGAL_POLICY_VERSION=${profile.legalPolicyVersion}`);
requireContract(String(env.TERMS_EFFECTIVE_DATE || '').trim() === profile.termsEffectiveDate,
`TERMS_EFFECTIVE_DATE=${profile.termsEffectiveDate}`);
requireContract(isExplicitTrue(env.PAYMENTS_ENABLED) && config?.paymentsEnabled === true,
'PAYMENTS_ENABLED=true');
requireContract(String(env.GAME_MODE || '').trim() === profile.gameMode,
`GAME_MODE=${profile.gameMode}`);
const pausedWow = isWowPaymentsPaused(env);
requireContract(pausedWow
? config?.paymentsEnabled === false
: isExplicitTrue(env.PAYMENTS_ENABLED) && config?.paymentsEnabled === true,
pausedWow ? 'PAYMENTS_ENABLED=false with payments disabled' : 'PAYMENTS_ENABLED=true');
requireContract(String(env.GAME_MODE || '').trim() === (pausedWow ? 'FREE' : profile.gameMode),
`GAME_MODE=${pausedWow ? 'FREE' : profile.gameMode}`);
requireContract(isExplicitTrue(env.PAID_ACKNOWLEDGEMENT_REQUIRED),
'PAID_ACKNOWLEDGEMENT_REQUIRED=true');
requireContract(isExplicitTrue(env.FREE_PLAY_ENABLED), 'FREE_PLAY_ENABLED=true');
@@ -162,12 +172,27 @@ function validateOperatedProductProfile(env = process.env, config = {}) {
if (id === OPERATED_PRODUCT_PROFILE_IDS.WOW_PRESTIGE) {
requireContract(cryptoType === 'WOW', 'CRYPTO_TYPE=WOW');
requireContract(network === 'mainnet', 'MONERO_NETWORK=mainnet');
requireContract(String(env.PAYMENT_MODES || '').trim().toLowerCase() === 'credits',
'PAYMENT_MODES=credits');
if (pausedWow) {
requireContract(Object.prototype.hasOwnProperty.call(env, 'PAYMENT_MODES')
&& String(env.PAYMENT_MODES).trim() === '', 'explicit empty PAYMENT_MODES');
requireContract(isExplicitFalse(env.CREDITS_ENABLED)
&& config?.modes?.credits?.enabled === false, 'CREDITS_ENABLED=false');
requireContract(!env.CREDITS_PAYOUT_ENABLED || isExplicitFalse(env.CREDITS_PAYOUT_ENABLED),
'CREDITS_PAYOUT_ENABLED=false when set');
requireContract(isExplicitFalse(env.EARLY_ENTRY_CREDITS_MODE),
'EARLY_ENTRY_CREDITS_MODE=false');
requireContract(String(env.FINANCIAL_EVENT_SINK_URL || '').trim() === '',
'empty FINANCIAL_EVENT_SINK_URL');
requireContract(String(env.FINANCIAL_EVENT_SINK_TOKEN || '').trim() === '',
'empty FINANCIAL_EVENT_SINK_TOKEN');
} else {
requireContract(String(env.PAYMENT_MODES || '').trim().toLowerCase() === 'credits',
'PAYMENT_MODES=credits');
requireContract(isExplicitTrue(env.CREDITS_ENABLED)
&& config?.modes?.credits?.enabled === true, 'CREDITS_ENABLED=true');
}
requireContract(isExplicitFalse(env.DIRECT_PAYMENT_ENABLED)
&& config?.modes?.direct?.enabled === false, 'DIRECT_PAYMENT_ENABLED=false');
requireContract(isExplicitTrue(env.CREDITS_ENABLED)
&& config?.modes?.credits?.enabled === true, 'CREDITS_ENABLED=true');
requireContract(isExplicitFalse(env.PAYOUTS_ENABLED)
&& config?.payouts?.enabled === false, 'PAYOUTS_ENABLED=false');
requireContract(isExplicitFalse(env.DIRECT_PAYOUTS_ENABLED)
@@ -204,6 +229,7 @@ module.exports = {
OPERATED_PRODUCT_PROFILE_IDS,
SOURCE_SOFTWARE,
getOperatedProductProfile,
isWowPaymentsPaused,
selectPublicPaidModeDescriptors,
selectedProfileId,
validateOperatedProductProfile
+34 -29
View File
@@ -24,7 +24,7 @@ const { listMatchRulesets, resolveMatchRuleset } = require('./rulesets');
const { matchPayoutAdmissionPolicy } = require('../network/matchEconomyPolicy');
const { isSmirkEnabled } = require('../auth/smirkPolicy');
const { reservePayoutCapacity } = require('../services/payoutAdmissionService');
const { getOperatedProductProfile } = require('../config/operatedProductProfiles');
const { getOperatedProductProfile, isWowPaymentsPaused } = require('../config/operatedProductProfiles');
// A wallet "not enough (unlocked) money" error is raised BEFORE the tx is broadcast, so it is
// safe to retry (no double-pay risk), unlike an ambiguous post-broadcast failure. Monero locks
@@ -350,6 +350,7 @@ class GameModeManager {
* enforce it again in createPaymentRequest for non-Socket.IO callers.
*/
isPaymentIntakeEnabled(paymentType) {
if (isWowPaymentsPaused(process.env)) return false;
if (!this._isFinancialAdmissionAllowed()) return false;
if (!this.paymentsEnabled) return false;
if (paymentType === 'single_game') return Boolean(this.directModeEnabled);
@@ -662,6 +663,7 @@ class GameModeManager {
_getMatchEconomies() {
const out = { free: true };
if (isWowPaymentsPaused(process.env)) return out;
if (this.creditsModeEnabled) {
out.credits_prestige = true;
}
@@ -1232,16 +1234,17 @@ class GameModeManager {
* @returns {object} { mode, canUseCredits, canUseDirect, hasCredits, creditsBalance }
*/
getEffectiveModeForUser(user) {
const pausedWow = isWowPaymentsPaused(process.env);
const hasCredits = (user?.credits || 0) >= this.creditsPerGameCost;
const bothModesEnabled = this.directModeEnabled && this.creditsModeEnabled;
const bothModesEnabled = !pausedWow && this.directModeEnabled && this.creditsModeEnabled;
// Determine available options
const canUseCredits = this.creditsModeEnabled && hasCredits;
const canUseDirect = this.directModeEnabled;
const canUseCredits = !pausedWow && this.creditsModeEnabled && hasCredits;
const canUseDirect = !pausedWow && this.directModeEnabled;
// Determine effective mode
let effectiveMode;
if (!this.paymentsEnabled) {
if (!this.paymentsEnabled || pausedWow) {
effectiveMode = 'FREE';
} else if (bothModesEnabled) {
// Mixed mode: prefer based on config and availability
@@ -1269,13 +1272,13 @@ class GameModeManager {
creditsRequired: this.creditsPerGameCost,
bothModesEnabled,
preferCreditsFirst: this.preferCreditsFirst,
creditsPayoutsEnabled: this.isPayoutEnabledForMode('PAID_CREDITS'),
directPayoutsEnabled: this.isPayoutEnabledForMode('PAID_SINGLE')
creditsPayoutsEnabled: !pausedWow && this.isPayoutEnabledForMode('PAID_CREDITS'),
directPayoutsEnabled: !pausedWow && this.isPayoutEnabledForMode('PAID_SINGLE')
};
}
getPublicPreferredPaymentMode(user) {
if (!this.paymentsEnabled) {
if (!this.paymentsEnabled || isWowPaymentsPaused(process.env)) {
return 'free';
}
@@ -1344,7 +1347,7 @@ class GameModeManager {
});
}
if (this.creditsModeEnabled) {
if (this.creditsModeEnabled && !isWowPaymentsPaused(process.env)) {
const pkg = this.getPrimaryCreditPackage();
options.push({
type: 'buy_credits',
@@ -2420,7 +2423,8 @@ class GameModeManager {
: null;
// Serialize atomic prices as exact numbers when safe, decimal strings otherwise.
const packages = this.configSnapshot?.modes?.credits?.packages || [];
const pausedWow = isWowPaymentsPaused(process.env);
const packages = pausedWow ? [] : (this.configSnapshot?.modes?.credits?.packages || []);
const creditPackages = packages.map(pkg => ({
id: pkg.id,
label: pkg.label || pkg.id,
@@ -2435,7 +2439,7 @@ class GameModeManager {
2
)
}));
const cosmeticProducts = this.getCosmeticProducts().map(product => ({
const cosmeticProducts = (pausedWow ? [] : this.getCosmeticProducts()).map(product => ({
id: product.id,
label: product.label || product.id,
price: money.toSafe(money.toBig(product.price)),
@@ -2453,7 +2457,7 @@ class GameModeManager {
commit: null
},
operatedProductProfileId: getOperatedProductProfile(process.env)?.id || null,
mode: this.gameMode,
mode: pausedWow ? 'FREE' : this.gameMode,
freePlayEnabled: !!this.freePlayEnabled, // players may choose free play even on a paid instance
cryptoType: this.cryptoType,
currencyLabel: this.currencyLabel, // sXMR on stagenet, XMR on mainnet, WOW for Wownero
@@ -2461,40 +2465,41 @@ class GameModeManager {
network: this.network,
isTestNetwork: this.isTestNetwork,
testnetWarning: testnetWarning,
singleGamePrice: this.singleGamePrice,
singleGamePriceFormatted: this.formatAtomicHuman(this.singleGamePrice, 2),
creditsPackagePrice: this.creditsPackagePrice,
creditsPerGame: this.creditsPerGameCost,
singleGamePrice: pausedWow ? 0 : this.singleGamePrice,
singleGamePriceFormatted: pausedWow ? null : this.formatAtomicHuman(this.singleGamePrice, 2),
creditsPackagePrice: pausedWow ? 0 : this.creditsPackagePrice,
creditsPerGame: pausedWow ? 0 : this.creditsPerGameCost,
creditPackages: creditPackages,
cosmeticProducts: cosmeticProducts,
creditsPayoutBaseValue: this.creditsPayoutBaseValue,
paymentsEnabled: this.paymentsEnabled,
creditsPayoutBaseValue: pausedWow ? 0 : this.creditsPayoutBaseValue,
paymentsEnabled: pausedWow ? false : this.paymentsEnabled,
freePlayEnabled: this.freePlayEnabled,
directModeEnabled: this.directModeEnabled,
creditsModeEnabled: this.creditsModeEnabled,
payoutsEnabled: this.payoutsEnabled,
directPayoutsEnabled: this.isPayoutEnabledForMode('PAID_SINGLE'),
creditsPayoutsEnabled: this.isPayoutEnabledForMode('PAID_CREDITS'),
payoutMultipliers: {
directModeEnabled: pausedWow ? false : this.directModeEnabled,
creditsModeEnabled: pausedWow ? false : this.creditsModeEnabled,
payoutsEnabled: pausedWow ? false : this.payoutsEnabled,
directPayoutsEnabled: pausedWow ? false : this.isPayoutEnabledForMode('PAID_SINGLE'),
creditsPayoutsEnabled: pausedWow ? false : this.isPayoutEnabledForMode('PAID_CREDITS'),
payoutMultipliers: pausedWow ? { direct: null, credits: null } : {
direct: this.getImplementedPayoutMultipliersForMode('PAID_SINGLE'),
credits: this.getImplementedPayoutMultipliersForMode('PAID_CREDITS')
},
// Exact server-calculated amounts. The browser must never recreate money promises
// with floating point multiplier arithmetic.
payoutOutcomes: {
payoutOutcomes: pausedWow ? { direct: null, credits: null } : {
direct: this._publicPayoutOutcomes('PAID_SINGLE'),
credits: this._publicPayoutOutcomes('PAID_CREDITS')
},
features: {
paymentRequired: this.paymentsEnabled,
creditsSystem: this.creditsModeEnabled,
payouts: this.isPayoutEnabledForMode('PAID_SINGLE')
paymentRequired: pausedWow ? false : this.paymentsEnabled,
creditsSystem: pausedWow ? false : this.creditsModeEnabled,
payouts: !pausedWow && (this.isPayoutEnabledForMode('PAID_SINGLE')
|| this.isPayoutEnabledForMode('PAID_CREDITS')
)
},
earlyEntry: {
enabled: this.configSnapshot?.earlyEntry?.enabled ?? false,
allowInFreeMode: this.configSnapshot?.earlyEntry?.allowInFreeMode ?? false,
allowInCreditsMode: this.configSnapshot?.earlyEntry?.allowInCreditsMode ?? false
allowInCreditsMode: pausedWow ? false : (this.configSnapshot?.earlyEntry?.allowInCreditsMode ?? false)
},
// Smirk wallet integration. Smirk only works on mainnet, so it is forced off on
// any test network (stagenet/testnet) regardless of SMIRK_ENABLED.
+2
View File
@@ -18,6 +18,7 @@
const { normalizeError } = require('../utils/errors');
const { createFinancialRecoveryError } = require('../utils/financialRecoveryError');
const { matchPayoutAdmissionPolicy } = require('./matchEconomyPolicy');
const { isWowPaymentsPaused } = require('../config/operatedProductProfiles');
const crypto = require('crypto');
const { PAID_FREEZE_VERSION, buildPaidEntrantFreeze } = require('./matchFairness');
@@ -373,6 +374,7 @@ class MatchQueue {
_isEconomyAvailable(economy) {
if (!this.enabled || !this._validateEconomy(economy)) return false;
if (economy !== 'free' && isWowPaymentsPaused(process.env)) return false;
if (economy === 'crypto_race') {
// Never let an injected/legacy queue bypass the crypto admission contract. This is
// evaluated on every join/drain so a hot kill-switch takes effect immediately.
+3 -1
View File
@@ -68,7 +68,9 @@ function operatorAndSoftwareStatus(disclosure) {
const operated = disclosure.operatedProduct;
const software = disclosure.software || {};
const relationship = operated
? `<p><strong>Operated-product scope:</strong> ${escapeHtml(operated.scopeNotice)}</p>${operated.noRealValueNotice
? `<p><strong>Operated-product scope:</strong> ${escapeHtml(operated.scopeNotice)}</p>${operated.availabilityNotice
? `<p><strong>Current availability:</strong> ${escapeHtml(operated.availabilityNotice)}</p>`
: ''}${operated.noRealValueNotice
? `<p class="no-real-value"><strong>${escapeHtml(operated.noRealValueNotice)}</strong></p>`
: ''}<p>${escapeHtml(software.operatedBoundaryNotice)}</p><p>${escapeHtml(software.thirdPartyNotice)}</p>`
: `<p><strong>Independent deployment:</strong> ${escapeHtml(software.thirdPartyNotice)}</p><p>${escapeHtml(software.operatedBoundaryNotice)}</p>`;