mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-17 22:31:26 +02:00
fix: make monerogue direct-entry only
This commit is contained in:
@@ -9,7 +9,7 @@ Such Software (`apps@such.software`) operates only these two public products:
|
||||
| Service | Operated scope |
|
||||
|---|---|
|
||||
| `play.wowne.ro` | Wownero mainnet free play and pay-for-credits leaderboard/prestige play. Credits are non-redeemable; there are no prizes, cash-out, or payouts, and the service is not offered or marketed as gambling. Legal classification depends on applicable law. |
|
||||
| `monerogue.app` | Monero **stagenet only**. Single-player 2×/3× outcomes are test gambling mechanics using test coins with **NO REAL VALUE**. Never send mainnet XMR. Crypto-match payouts remain off. |
|
||||
| `monerogue.app` | Monero **stagenet only**, with direct solo entry only. Single-player 2×/3× outcomes are test gambling mechanics using test coins with **NO REAL VALUE**. Purchased-credit entry and crypto-match payouts are off. Never send mainnet XMR. |
|
||||
|
||||
The repository remains generally self-hostable under the MIT License. A third-party operator is
|
||||
solely responsible for its deployment, product, legal compliance, funds, players, claims, and
|
||||
@@ -52,9 +52,10 @@ Open http://localhost:3000 to play.
|
||||
| **PAID_CREDITS** | Purchased credit | Profile-controlled | Paid prestige |
|
||||
|
||||
The exact Such Software production profiles are `src/.env.mainnet.example` (credits only, every
|
||||
payout path off) and `src/.env.stagenet.example` (stagenet 2×/3× solo test payouts, crypto-match
|
||||
payouts off). Each opts into a startup contract with `OPERATED_PRODUCT_PROFILE`; preflight refuses
|
||||
network, operator-identity, or economic-scope drift. Generic self-hosts leave that variable unset.
|
||||
payout path off) and `src/.env.stagenet.example` (direct-entry-only stagenet 2×/3× solo test
|
||||
payouts, purchased credits and crypto-match payouts off). Each opts into a startup contract with
|
||||
`OPERATED_PRODUCT_PROFILE`; preflight refuses network, operator-identity, or economic-scope drift.
|
||||
Generic self-hosts leave that variable unset.
|
||||
|
||||
## Match Mode (Multiplayer Races)
|
||||
|
||||
|
||||
+3
-2
@@ -5,8 +5,9 @@ This guide covers deploying Wownerogue to a production environment.
|
||||
## Operated profiles versus independent deployments
|
||||
|
||||
Such Software (`apps@such.software`) operates only `play.wowne.ro` (Wownero mainnet
|
||||
pay-for-credits leaderboard/prestige, no prize/payout/cash-out and not marketed as gambling) and `monerogue.app` (Monero
|
||||
stagenet **NO REAL VALUE** 2×/3× solo test gambling mechanics, with crypto-match payouts off).
|
||||
pay-for-credits leaderboard/prestige, no prize/payout/cash-out and not marketed as gambling) and
|
||||
`monerogue.app` (Monero stagenet direct-entry-only **NO REAL VALUE** 2×/3× solo test gambling
|
||||
mechanics, with purchased-credit entry and crypto-match payouts off).
|
||||
Classification of any product under applicable law requires jurisdiction-specific advice.
|
||||
Their reviewed environment templates set `OPERATED_PRODUCT_PROFILE`, so preflight/startup rejects
|
||||
scope drift. Independent MIT deployments must leave that variable unset and identify their actual
|
||||
|
||||
+11
-10
@@ -148,8 +148,8 @@ DIFFICULTY_PRESET=casino
|
||||
|
||||
# --- payments + free play ---
|
||||
PAYMENTS_ENABLED=true
|
||||
PAYMENT_MODES=direct,credits
|
||||
FREE_PLAY_ENABLED=true # both boards available
|
||||
PAYMENT_MODES=direct
|
||||
FREE_PLAY_ENABLED=true # free and direct solo boards available
|
||||
|
||||
# --- direct (pay per game) WITH payouts (fake stagenet money) ---
|
||||
DIRECT_PAYMENT_ENABLED=true
|
||||
@@ -159,17 +159,18 @@ DIRECT_PAYOUTS_ENABLED=true
|
||||
DIRECT_PAYOUT_ESCAPE=2.0
|
||||
DIRECT_PAYOUT_TREASURE=3.0
|
||||
|
||||
# --- credits WITH payouts ---
|
||||
CREDITS_ENABLED=true
|
||||
# --- purchased credits are outside the operated Monerogue profile ---
|
||||
CREDITS_ENABLED=false
|
||||
CREDITS_PER_GAME=1
|
||||
CREDITS_REQUIRES_ADDRESS=true
|
||||
CREDITS_PAYOUTS_ENABLED=true
|
||||
CREDITS_REQUIRES_ADDRESS=false
|
||||
CREDITS_PAYOUTS_ENABLED=false
|
||||
# Inert compatibility values; no credit purchase or credit payout path may expose them.
|
||||
CREDITS_PAYOUT_BASE=10000000000
|
||||
CREDITS_PAYOUT_ESCAPE=2.0
|
||||
CREDITS_PAYOUT_TREASURE=3.0
|
||||
CREDITS_PACKAGES=[{"id":"small","credits":10,"price":"90000000000","bonus":0},{"id":"medium","credits":20,"price":"170000000000","bonus":2}]
|
||||
ALLOW_MIXED_MODE=true
|
||||
PREFER_CREDITS_FIRST=true
|
||||
ALLOW_MIXED_MODE=false
|
||||
PREFER_CREDITS_FIRST=false
|
||||
|
||||
# --- payouts --- (verify the unlocked stagenet bankroll before every rollout)
|
||||
PAYOUTS_ENABLED=true
|
||||
@@ -180,8 +181,8 @@ PAYOUT_BATCH_INTERVAL=300
|
||||
PAYOUT_MAX_RETRIES=3
|
||||
BALANCE_WARN=100000000000 # warn below 0.10 XMR unlocked
|
||||
BALANCE_CRITICAL=50000000000 # stop accepting new payout liability below 0.05 XMR
|
||||
# Tip: lower DIRECT_GAME_PRICE/CREDITS_PAYOUT_BASE to 0.001 XMR (1000000000) for many more
|
||||
# demo plays per the small bankroll; refill from a stagenet faucet as needed.
|
||||
# Tip: lower DIRECT_GAME_PRICE to 0.001 XMR (1000000000) for many more demo plays per the
|
||||
# small bankroll; refill from a stagenet faucet as needed.
|
||||
|
||||
# --- wallet RPC (monero stagenet) ---
|
||||
PRIMARY_WALLET_ENDPOINT=http://127.0.0.1:38083
|
||||
|
||||
@@ -11,7 +11,7 @@ Such Software (`apps@such.software`) operates only:
|
||||
| Profile | Public service | Exact scope |
|
||||
|---|---|---|
|
||||
| `such-play-wow-prestige` | `play.wowne.ro` | Wownero mainnet free play and pay-for-credits leaderboard/prestige. Credits are non-redeemable; there are no prizes, payouts, cash-out, or crypto racing, and the service is not offered/marketed as gambling. Legal classification depends on applicable law. |
|
||||
| `such-monerogue-stagenet` | `monerogue.app` | Monero stagenet only. Single-player 2×/3× test gambling mechanics using test coins with **NO REAL VALUE**. Never mainnet XMR and no crypto-match payouts. |
|
||||
| `such-monerogue-stagenet` | `monerogue.app` | Monero stagenet direct-entry solo only. Single-player 2×/3× test gambling mechanics use test coins with **NO REAL VALUE**. Purchased-credit entry and crypto-match payouts are off. Never mainnet XMR. |
|
||||
|
||||
The corresponding `.env.mainnet.example` and `.env.stagenet.example` files opt into a fail-closed
|
||||
startup contract with `OPERATED_PRODUCT_PROFILE`. Preflight and normal startup reject any drift in
|
||||
@@ -90,8 +90,9 @@ The acknowledgement is a player statement, not proof of identity, age, capacity,
|
||||
| XMR stagenet/testnet | Valueless test currency; explicit warning never to send mainnet XMR |
|
||||
|
||||
For the operated products, the generic rows narrow further: `play.wowne.ro` exposes purchased
|
||||
credits only, offers no payout/cash-out, and is not marketed as gambling; `monerogue.app` labels its 2×/3× stagenet mechanics as
|
||||
**NO REAL VALUE** and keeps crypto-match payouts disabled.
|
||||
credits only, offers no payout/cash-out, and is not marketed as gambling; `monerogue.app` labels
|
||||
its direct-entry 2×/3× stagenet mechanics as **NO REAL VALUE** and keeps purchased-credit entry and
|
||||
crypto-match payouts disabled.
|
||||
|
||||
## Unresolved launch decisions
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
# XMR stagenet financial canary
|
||||
|
||||
`src/scripts/stagenet-financial-canary.js` is the release-gate harness for Monerogue's two paid solo settlement paths. It is intentionally narrow: one new anonymous user, one invoice, at most one funding transfer, one bounded game, and one payout. It supports these scenarios:
|
||||
`src/scripts/stagenet-financial-canary.js` is the release-gate harness for Monerogue's two
|
||||
direct-entry solo outcomes. Purchased-credit entry is outside the operated product profile. The
|
||||
harness is intentionally narrow: one new anonymous user, one invoice, at most one funding
|
||||
transfer, one bounded game, and one payout. It supports these scenarios:
|
||||
|
||||
| `E2E_SCENARIO` | Entry | Required outcome | Exact payout |
|
||||
|---|---|---|---|
|
||||
| `direct-2x` | one `single_game` invoice | escape without treasure | invoice amount × 2 |
|
||||
| `credits-3x` | one pure credits-package invoice, then one `PAID_CREDITS` entry | collect treasure, then escape | configured credits payout base × 3 |
|
||||
| `direct-3x` | one `single_game` invoice | collect treasure, then escape | invoice amount × 3 |
|
||||
|
||||
Stagenet XMR is valueless test currency. Never send mainnet XMR to a stagenet address. This harness refuses mainnet-facing application state and funding-wallet addresses, but that guard is not a substitute for checking the wallet and daemon commands yourself.
|
||||
|
||||
@@ -15,14 +18,19 @@ The harness fails closed unless all of these are true:
|
||||
|
||||
- The application URL explicitly names `127.0.0.1`, `localhost`, or `::1` on dedicated port `3102`. It cannot target the public router, LAN hostname, or Nebula address.
|
||||
- Readiness, public disclosures, REST mode data, and Socket.IO mode data all identify real XMR stagenet with payments and payout dispatch enabled.
|
||||
- Both paid solo modes advertise exact 2× escape and 3× treasure multipliers.
|
||||
- Direct solo is enabled with exact 2× escape and 3× treasure multipliers. Purchased-credit solo
|
||||
and its payout path are disabled.
|
||||
- The current `/api/disclosures` response requires the canonical five-field acknowledgement. The harness refetches it immediately before the value-bearing action and echoes exactly `policyVersion`, `ageEligible`, `termsRead`, `riskAccepted`, and `testnetUnderstood`.
|
||||
- The selected PostgreSQL database name contains `canary`, `e2e`, and the selected scenario (`direct` or `credits`). Its connection must be localhost, the harness session is read-only, current financial migrations must exist, and the database must contain only the migration-seeded admin with no game or financial rows.
|
||||
- The selected PostgreSQL database name contains `canary`, `e2e`, and the selected scenario tag
|
||||
(`direct` for 2× or `treasure` for 3×). Its connection must be localhost, the harness session is
|
||||
read-only, current financial migrations must exist, and the database must contain only the
|
||||
migration-seeded admin with no game or financial rows.
|
||||
- Before any invoice, funding-wallet call, or transfer, a nonce challenge binds the application pool to the harness connection's exact PostgreSQL cluster identifier, database OID, and database name. A same-named database on another cluster does not pass.
|
||||
- The funding wallet RPC is localhost-only and either digest-authenticated or enabled by a separate explicit unauthenticated-local-RPC acknowledgement.
|
||||
- The funding wallet validates both its own address and the invoice as stagenet. `get_address_index` must prove the invoice does not belong to the funding wallet; this prevents a meaningless house-wallet self-payment.
|
||||
- Live execution needs three exact confirmations plus an atomic-unit transfer ceiling. The transfer gate is marked used before the sole `transfer` RPC call, so an ambiguous transport failure is never retried.
|
||||
- The bot is bounded to one dungeon depth, 3,000 moves, and eight minutes. The direct scenario treats treasure as blocked. The credits scenario treats the exit as blocked until treasure is collected.
|
||||
- The bot is bounded to one dungeon depth, 3,000 moves, and eight minutes. The 2× scenario treats
|
||||
treasure as blocked. The 3× scenario treats the exit as blocked until treasure is collected.
|
||||
- Wallet addresses, session tokens, proof seeds, transaction hashes, and credentials are never printed. Failure messages redact those shapes.
|
||||
|
||||
The harness never creates, restores, exports, migrates, or backs up a wallet. It never deploys code, changes router state, or edits fleet configuration.
|
||||
@@ -32,7 +40,7 @@ The harness never creates, restores, exports, migrates, or backs up a wallet. It
|
||||
Use two different stagenet wallets:
|
||||
|
||||
1. The canary application owns the normal house wallet used to create invoice subaddresses and dispatch payouts.
|
||||
2. A separate, low-balance funding wallet sends the entry/package payment and receives the payout.
|
||||
2. A separate, low-balance funding wallet sends the direct-entry payment and receives the payout.
|
||||
|
||||
Expose only the separate funding wallet RPC to the harness, on loopback. Do not give the harness the house-wallet RPC URL or credentials. Keep the funding balance no larger than needed for the selected invoice plus fees.
|
||||
|
||||
@@ -55,18 +63,16 @@ DUNGEON_LEVELS=1
|
||||
The canary instance must otherwise use the intended production money configuration, including:
|
||||
|
||||
- `CRYPTO_TYPE=XMR`, `MONERO_NETWORK=stagenet`, real stagenet daemon and house wallet RPC;
|
||||
- direct and credits paid modes enabled together;
|
||||
- direct and credits payouts enabled;
|
||||
- 2× escape and 3× treasure multipliers for both modes;
|
||||
- a positive `CREDITS_PAYOUT_BASE` whose 3× result is within the configured payout bounds;
|
||||
- a credits-only package (default id `small`) with no race entries, packs, or premium grant;
|
||||
- direct paid mode and direct payouts enabled;
|
||||
- purchased-credit mode and credit payouts disabled;
|
||||
- exact direct 2× escape and 3× treasure multipliers;
|
||||
- production paid-action acknowledgement enabled and current operator/legal disclosure metadata.
|
||||
|
||||
Use a fresh database name such as:
|
||||
|
||||
```text
|
||||
monerogue_canary_direct_e2e
|
||||
monerogue_canary_credits_e2e
|
||||
monerogue_canary_treasure_e2e
|
||||
```
|
||||
|
||||
First boot the release candidate against the new database with payout dispatch disabled solely to run startup migrations. Stop it after readiness. Then run database preflight. Finally boot the same release candidate and same database with the intended payout configuration on localhost port `3102`. Do not point a public router at this instance.
|
||||
@@ -81,7 +87,7 @@ CANARY_EXPECT_DATABASE=monerogue_canary_direct_e2e
|
||||
CANARY_DATABASE_NONCE_FILE=/run/credentials/canary/database-nonce
|
||||
```
|
||||
|
||||
Use the corresponding `credits` database name for that scenario. If none of the three
|
||||
Use the corresponding `treasure` database name for the 3× scenario. If none of the three
|
||||
`CANARY_DATABASE_*` variables is set, the endpoint is not registered. A partial configuration,
|
||||
non-XMR-stagenet process, non-production process, wrong port, or `DB_NAME` mismatch aborts startup.
|
||||
The endpoint signs its database identity and a fresh caller challenge; it never returns the nonce.
|
||||
@@ -125,7 +131,8 @@ E2E_DATABASE_URL_FILE=/run/credentials/canary/direct-database-url \
|
||||
npm --prefix src run canary:stagenet
|
||||
```
|
||||
|
||||
For the credits scenario, use `E2E_SCENARIO=credits-3x`, a database name containing `credits`, and its separate protected connection file.
|
||||
For the 3× scenario, use `E2E_SCENARIO=direct-3x`, a database name containing `treasure`, and its
|
||||
separate protected connection file.
|
||||
|
||||
## Live direct 2× escape
|
||||
|
||||
@@ -151,17 +158,18 @@ npm --prefix src run canary:stagenet
|
||||
|
||||
The harness binds a fresh fairness offer to the direct invoice before revealing its address, transfers the exact invoice amount, waits for receipt-backed confirmation and automatic game start, avoids treasure, escapes, and verifies an exact 2× payout.
|
||||
|
||||
## Live credits-package then 3× treasure escape
|
||||
## Live direct 3× treasure escape
|
||||
|
||||
Review the selected package's atomic price and set the ceiling accordingly. The default package id is `small`; override it with `E2E_CREDITS_PACKAGE_ID` only after confirming it is a pure credits-only package.
|
||||
Review the configured direct-entry atomic amount before setting the ceiling. As with the 2× run,
|
||||
the ceiling must be at least the advertised invoice amount and should equal that amount when
|
||||
practical.
|
||||
|
||||
```bash
|
||||
E2E_MODE=live-stagenet \
|
||||
E2E_SCENARIO=credits-3x \
|
||||
E2E_CREDITS_PACKAGE_ID=small \
|
||||
E2E_SCENARIO=direct-3x \
|
||||
E2E_TARGET=http://127.0.0.1:3102 \
|
||||
E2E_EXPECT_DATABASE=monerogue_canary_credits_e2e \
|
||||
E2E_DATABASE_URL_FILE=/run/credentials/canary/credits-database-url \
|
||||
E2E_EXPECT_DATABASE=monerogue_canary_treasure_e2e \
|
||||
E2E_DATABASE_URL_FILE=/run/credentials/canary/treasure-database-url \
|
||||
E2E_DATABASE_NONCE_FILE=/run/credentials/canary/database-nonce \
|
||||
E2E_FUNDING_RPC_URL=http://127.0.0.1:38083 \
|
||||
E2E_FUNDING_RPC_USER_FILE=/run/credentials/canary/funding-rpc-user \
|
||||
@@ -169,12 +177,15 @@ E2E_FUNDING_RPC_PASSWORD_FILE=/run/credentials/canary/funding-rpc-password \
|
||||
E2E_MAX_TRANSFER_ATOMIC=REPLACE_WITH_REVIEWED_ATOMIC_CEILING \
|
||||
E2E_FEE_CUSHION_ATOMIC=1000000000 \
|
||||
E2E_CONFIRM=I_UNDERSTAND_THIS_BROADCASTS_ONE_XMR_STAGENET_TRANSFER \
|
||||
E2E_SCENARIO_CONFIRM=CREDITS_PACKAGE_THEN_3X_TREASURE_ESCAPE \
|
||||
E2E_SCENARIO_CONFIRM=DIRECT_3X_TREASURE_ESCAPE \
|
||||
E2E_CANARY_PROFILE=EASY_STATIC_MONSTER_ONE_LEVEL \
|
||||
npm --prefix src run canary:stagenet
|
||||
```
|
||||
|
||||
The harness purchases the package, verifies the exact confirmed grant and balance update, consumes a new fairness offer in `auto_start`, spends exactly one configured game credit, collects treasure before allowing the exit, and verifies an exact 3× payout based on `CREDITS_PAYOUT_BASE` (not the package price).
|
||||
The harness binds a fresh fairness offer to the direct invoice before revealing its address,
|
||||
transfers the exact invoice amount, waits for receipt-backed confirmation and automatic game
|
||||
start, collects treasure before allowing the exit, and verifies an exact 3× payout based on the
|
||||
invoice amount.
|
||||
|
||||
## Exact settlement assertions
|
||||
|
||||
@@ -187,9 +198,9 @@ After the funding wallet observes the incoming payout, the harness checks the re
|
||||
- one completed payout with the exact amount, multiplier, reason, payout address, and transaction evidence;
|
||||
- no refunds, late reviews, matches, match queues, race-entry ledgers, or race-entry lots.
|
||||
|
||||
The direct scenario additionally requires exactly the `direct_entry +1` and `game_entry -1` credit-ledger rows, a zero balance, unified purchase progress of one, the game linked to the fairness-bound payment, and no product-entitlement marker.
|
||||
|
||||
The credits scenario additionally requires exactly one payment-linked `package_purchase` grant, one `game_entry` debit, the exact final balance, one active payment-entitlement marker matching the advertised grant, and a `PAID_CREDITS` game with no direct `payment_id`.
|
||||
Each scenario additionally requires exactly the `direct_entry +1` and `game_entry -1`
|
||||
credit-ledger rows, a zero balance, unified purchase progress of one, the game linked to the
|
||||
fairness-bound payment, and no product-entitlement marker.
|
||||
|
||||
## Failure handling
|
||||
|
||||
|
||||
@@ -36,12 +36,13 @@ PAID_ACKNOWLEDGEMENT_REQUIRED=true
|
||||
# Optional plain-language notice supplied after jurisdiction-specific legal review.
|
||||
# RESTRICTED_LOCATIONS_NOTICE=
|
||||
|
||||
# The original solo game remains available for free and through both test-coin entry paths.
|
||||
# The original solo game remains available for free and direct test-coin entry.
|
||||
# Purchased-credit entry is outside this operated profile and stays fail-closed.
|
||||
PAYMENTS_ENABLED=true
|
||||
FREE_PLAY_ENABLED=true
|
||||
PAYMENT_MODES=direct,credits
|
||||
ALLOW_MIXED_MODE=true
|
||||
PREFER_CREDITS_FIRST=true
|
||||
PAYMENT_MODES=direct
|
||||
ALLOW_MIXED_MODE=false
|
||||
PREFER_CREDITS_FIRST=false
|
||||
|
||||
# NO REAL VALUE: 0.01 stagenet XMR per direct run; escape pays 2x, treasure escape pays 3x.
|
||||
DIRECT_PAYMENT_ENABLED=true
|
||||
@@ -52,10 +53,11 @@ DIRECT_PAYOUTS_ENABLED=true
|
||||
DIRECT_PAYOUT_ESCAPE=2.0
|
||||
DIRECT_PAYOUT_TREASURE=3.0
|
||||
|
||||
CREDITS_ENABLED=true
|
||||
CREDITS_ENABLED=false
|
||||
CREDITS_PER_GAME=1
|
||||
CREDITS_REQUIRES_ADDRESS=true
|
||||
CREDITS_PAYOUTS_ENABLED=true
|
||||
CREDITS_REQUIRES_ADDRESS=false
|
||||
CREDITS_PAYOUTS_ENABLED=false
|
||||
# Inert compatibility values: the operated profile rejects credit purchase and credit payout paths.
|
||||
CREDITS_PAYOUT_BASE=10000000000
|
||||
CREDITS_PAYOUT_ESCAPE=2.0
|
||||
CREDITS_PAYOUT_TREASURE=3.0
|
||||
|
||||
@@ -44,8 +44,8 @@ const PROFILES = Object.freeze({
|
||||
operatorName: 'Such Software',
|
||||
operatorContactUrl: SOURCE_SOFTWARE.contactUrl,
|
||||
operatorContactLabel: SOURCE_SOFTWARE.contactLabel,
|
||||
scopeNotice: 'Such Software operates monerogue.app only with Monero stagenet test coins. Its single-player 2×/3× outcomes are test gambling mechanics; no real-money or mainnet gambling is offered.',
|
||||
commerceSummary: 'NO REAL VALUE — monerogue.app uses Monero stagenet test coins only. Single-player qualifying outcomes pay 2× or 3× in test coins; never send mainnet XMR.',
|
||||
scopeNotice: 'Such Software operates monerogue.app only with direct-entry Monero stagenet test coins. Its single-player 2×/3× outcomes are test gambling mechanics; purchased-credit entry is disabled, and no real-money or mainnet gambling is offered.',
|
||||
commerceSummary: 'NO REAL VALUE — monerogue.app uses direct-entry Monero stagenet test coins only. Single-player qualifying outcomes pay 2× or 3× in test coins. Purchased-credit entry is off. Never send mainnet XMR.',
|
||||
noRealValueNotice: 'NO REAL VALUE — Monero stagenet coins are test data, not money, deposits, redeemable prizes, or a promise of value. Never send mainnet XMR to this service.'
|
||||
})
|
||||
});
|
||||
@@ -158,22 +158,20 @@ function validateOperatedProductProfile(env = process.env, config = {}) {
|
||||
if (id === OPERATED_PRODUCT_PROFILE_IDS.XMR_STAGENET) {
|
||||
requireContract(cryptoType === 'XMR', 'CRYPTO_TYPE=XMR');
|
||||
requireContract(network === 'stagenet', 'MONERO_NETWORK=stagenet');
|
||||
requireContract(String(env.PAYMENT_MODES || '').split(',').map(value => value.trim().toLowerCase()).join(',') === 'direct,credits',
|
||||
'PAYMENT_MODES=direct,credits');
|
||||
requireContract(String(env.PAYMENT_MODES || '').trim().toLowerCase() === 'direct',
|
||||
'PAYMENT_MODES=direct');
|
||||
requireContract(isExplicitTrue(env.DIRECT_PAYMENT_ENABLED)
|
||||
&& config?.modes?.direct?.enabled === true, 'DIRECT_PAYMENT_ENABLED=true');
|
||||
requireContract(isExplicitTrue(env.CREDITS_ENABLED)
|
||||
&& config?.modes?.credits?.enabled === true, 'CREDITS_ENABLED=true');
|
||||
requireContract(isExplicitFalse(env.CREDITS_ENABLED)
|
||||
&& config?.modes?.credits?.enabled === false, 'CREDITS_ENABLED=false');
|
||||
requireContract(isExplicitTrue(env.PAYOUTS_ENABLED)
|
||||
&& config?.payouts?.enabled === true, 'PAYOUTS_ENABLED=true');
|
||||
requireContract(isExplicitTrue(env.DIRECT_PAYOUTS_ENABLED)
|
||||
&& config?.payouts?.rules?.direct?.enabled === true, 'DIRECT_PAYOUTS_ENABLED=true');
|
||||
requireContract(isExplicitTrue(env.CREDITS_PAYOUTS_ENABLED)
|
||||
&& config?.payouts?.rules?.credits?.enabled === true, 'CREDITS_PAYOUTS_ENABLED=true');
|
||||
requireContract(isExplicitFalse(env.CREDITS_PAYOUTS_ENABLED)
|
||||
&& config?.payouts?.rules?.credits?.enabled === false, 'CREDITS_PAYOUTS_ENABLED=false');
|
||||
requireContract(exactMultiplier(env.DIRECT_PAYOUT_ESCAPE, 2), 'DIRECT_PAYOUT_ESCAPE=2');
|
||||
requireContract(exactMultiplier(env.DIRECT_PAYOUT_TREASURE, 3), 'DIRECT_PAYOUT_TREASURE=3');
|
||||
requireContract(exactMultiplier(env.CREDITS_PAYOUT_ESCAPE, 2), 'CREDITS_PAYOUT_ESCAPE=2');
|
||||
requireContract(exactMultiplier(env.CREDITS_PAYOUT_TREASURE, 3), 'CREDITS_PAYOUT_TREASURE=3');
|
||||
requireContract(isExplicitFalse(env.ALLOW_MAINNET_PAYOUTS), 'ALLOW_MAINNET_PAYOUTS=false');
|
||||
}
|
||||
|
||||
|
||||
@@ -78,7 +78,9 @@ function soloLeaderboardPolicy(profileId, board) {
|
||||
}
|
||||
if (profileId === OPERATED_PRODUCT_PROFILE_IDS.XMR_STAGENET) {
|
||||
return Object.freeze({
|
||||
gameModes: Object.freeze(['PAID_SINGLE', 'PAID_CREDITS']),
|
||||
// The operated stagenet product accepts direct entries only. Historical credit
|
||||
// package rows are not part of its current Hall of Champions.
|
||||
gameModes: Object.freeze(['PAID_SINGLE']),
|
||||
excludeMatchGenerated: true
|
||||
});
|
||||
}
|
||||
|
||||
@@ -50,15 +50,15 @@ const SCENARIOS = Object.freeze({
|
||||
payoutReason: 'escape',
|
||||
scenarioConfirm: 'DIRECT_2X_ESCAPE'
|
||||
}),
|
||||
'credits-3x': Object.freeze({
|
||||
id: 'credits-3x',
|
||||
databaseTag: 'credits',
|
||||
paymentType: 'credits_package',
|
||||
gameMode: 'PAID_CREDITS',
|
||||
'direct-3x': Object.freeze({
|
||||
id: 'direct-3x',
|
||||
databaseTag: 'treasure',
|
||||
paymentType: 'single_game',
|
||||
gameMode: 'PAID_SINGLE',
|
||||
collectTreasure: true,
|
||||
multiplier: 3,
|
||||
payoutReason: 'escape_with_treasure',
|
||||
scenarioConfirm: 'CREDITS_PACKAGE_THEN_3X_TREASURE_ESCAPE'
|
||||
scenarioConfirm: 'DIRECT_3X_TREASURE_ESCAPE'
|
||||
})
|
||||
});
|
||||
|
||||
@@ -120,7 +120,7 @@ function readConfiguration(env = process.env) {
|
||||
|
||||
const scenarioName = String(env.E2E_SCENARIO || '').trim();
|
||||
const scenario = scenarioName ? SCENARIOS[scenarioName] : null;
|
||||
if (scenarioName) assert(scenario, 'E2E_SCENARIO must be direct-2x or credits-3x');
|
||||
if (scenarioName) assert(scenario, 'E2E_SCENARIO must be direct-2x or direct-3x');
|
||||
if (mode !== 'preflight') {
|
||||
assert(scenario, 'E2E_SCENARIO is required for database and live modes');
|
||||
}
|
||||
@@ -134,15 +134,10 @@ function readConfiguration(env = process.env) {
|
||||
assertLocalUrl(target, 'E2E_TARGET', 3102);
|
||||
assert(target.pathname === '/' || target.pathname === '', 'E2E_TARGET must not contain a path');
|
||||
|
||||
const packageId = String(env.E2E_CREDITS_PACKAGE_ID || 'small').trim();
|
||||
assert(/^[a-z0-9][a-z0-9_-]{0,63}$/i.test(packageId),
|
||||
'E2E_CREDITS_PACKAGE_ID must be a simple product id');
|
||||
|
||||
return Object.freeze({
|
||||
mode,
|
||||
scenario,
|
||||
target,
|
||||
packageId,
|
||||
paymentTimeoutMs: boundedPositiveInt(env, 'E2E_PAYMENT_TIMEOUT_MS', 30 * 60 * 1000, 45 * 60 * 1000),
|
||||
payoutTimeoutMs: boundedPositiveInt(env, 'E2E_PAYOUT_TIMEOUT_MS', 15 * 60 * 1000, 30 * 60 * 1000),
|
||||
rpcPollMs: boundedPositiveInt(env, 'E2E_RPC_POLL_MS', 2000, 10000),
|
||||
@@ -184,12 +179,12 @@ function canonicalAcknowledgement(disclosure) {
|
||||
'commerce disclosure is not canonical XMR stagenet policy');
|
||||
assert(disclosure.service?.paymentsEnabled === true
|
||||
&& disclosure.service?.directPaidEntryEnabled === true
|
||||
&& disclosure.service?.paidCreditsEnabled === true
|
||||
&& disclosure.service?.paidCreditsEnabled === false
|
||||
&& disclosure.service?.soloPayoutsEnabled === true
|
||||
&& disclosure.service?.anyPayoutsEnabled === true
|
||||
&& disclosure.service?.paidPrestigeOnly === false
|
||||
&& disclosure.service?.cryptoMatchPayoutsEnabled === false,
|
||||
'commerce disclosure does not describe both payout-enabled paid solo modes');
|
||||
'commerce disclosure does not describe direct-only payout-enabled solo play');
|
||||
assert(disclosure.operatedProduct?.id === OPERATED_PROFILE_ID,
|
||||
'commerce disclosure is not the reviewed Such Software monerogue.app profile');
|
||||
assert(String(disclosure.operatedProduct?.scopeNotice || '').includes('2×/3×')
|
||||
@@ -241,18 +236,17 @@ function assertPublicModeContract(modes) {
|
||||
assert(modes?.soloEnabled === true && modes?.FREE?.enabled === true,
|
||||
'the original solo mode and free solo entry must be enabled');
|
||||
assert(modes?.PAID_SINGLE?.enabled === true
|
||||
&& modes?.PAID_CREDITS?.enabled === true,
|
||||
'both direct and credits paid solo modes must be enabled');
|
||||
&& modes?.PAID_CREDITS?.enabled === false
|
||||
&& Number(modes?.PAID_CREDITS?.payoutMultiplier) === 0,
|
||||
'direct solo must be enabled and purchased-credit solo must be disabled');
|
||||
assert(modes?.match?.enabled === true
|
||||
&& JSON.stringify(enabledEconomyIds(modes?.match?.economies))
|
||||
=== JSON.stringify(['credits_prestige', 'free']),
|
||||
'match mode must expose only the reviewed free and credits-prestige economies');
|
||||
for (const mode of ['PAID_SINGLE', 'PAID_CREDITS']) {
|
||||
assert(Number(modes?.[mode]?.payoutMultiplier?.escape) === 2,
|
||||
`${mode} escape payout is not exactly 2x`);
|
||||
assert(Number(modes?.[mode]?.payoutMultiplier?.escapeWithTreasure) === 3,
|
||||
`${mode} treasure payout is not exactly 3x`);
|
||||
}
|
||||
assert(Number(modes?.PAID_SINGLE?.payoutMultiplier?.escape) === 2,
|
||||
'PAID_SINGLE escape payout is not exactly 2x');
|
||||
assert(Number(modes?.PAID_SINGLE?.payoutMultiplier?.escapeWithTreasure) === 3,
|
||||
'PAID_SINGLE treasure payout is not exactly 3x');
|
||||
}
|
||||
|
||||
async function publicPreflight(config) {
|
||||
@@ -665,40 +659,6 @@ async function createDirectInvoice(socket, journal, acknowledgement, config, mod
|
||||
return { invoice, amount, offer, clientSeed };
|
||||
}
|
||||
|
||||
async function createCreditsInvoice(socket, journal, acknowledgement, config, packageInfo) {
|
||||
const after = journal.seq;
|
||||
socket.emit('request_payment', {
|
||||
type: 'credits_package',
|
||||
packageId: config.packageId,
|
||||
legalAcknowledgement: acknowledgement
|
||||
});
|
||||
const outcome = await journal.waitAny([
|
||||
{ name: 'payment_created' },
|
||||
{ name: 'payment_error' },
|
||||
{ name: 'balance_critical' },
|
||||
{ name: 'payment_review_required' },
|
||||
{ name: '$disconnect' }
|
||||
], 30000, after);
|
||||
assert(outcome.name === 'payment_created', `credits invoice creation failed (${outcome.name})`);
|
||||
const invoice = outcome.data || {};
|
||||
const amount = assertCommonInvoice(invoice, config, 'credits_package');
|
||||
assert(invoice.productId === config.packageId && invoice.package?.id === config.packageId,
|
||||
'credits invoice returned a different product');
|
||||
assert(amount === BigInt(String(packageInfo.price)),
|
||||
'credits invoice differs from advertised package price');
|
||||
assert(Number(invoice.grants?.credits) === packageInfo.grantedCredits,
|
||||
'credits invoice grant differs from advertised package grant');
|
||||
assert(invoice.fairness == null, 'credits purchase must not bind a game fairness offer');
|
||||
phase('pure credits package invoice matches its advertised product and grant');
|
||||
return {
|
||||
invoice,
|
||||
amount,
|
||||
grantedCredits: packageInfo.grantedCredits,
|
||||
creditsPerGame: packageInfo.creditsPerGame,
|
||||
payoutBase: packageInfo.payoutBase
|
||||
};
|
||||
}
|
||||
|
||||
function assertInvoiceNotOwned(ownership) {
|
||||
assert(ownership && ownership.error,
|
||||
'invoice belongs to the funding wallet; self-payment is forbidden');
|
||||
@@ -1017,7 +977,7 @@ async function waitForIncomingPayout(funding, txHash, expected, config) {
|
||||
throw new Error('separate funding wallet did not observe the payout before timeout');
|
||||
}
|
||||
|
||||
async function assertExactDatabaseCounts(db, scenario) {
|
||||
async function assertExactDatabaseCounts(db) {
|
||||
const result = await db.query(`
|
||||
SELECT
|
||||
(SELECT COUNT(*)::int FROM users) AS users,
|
||||
@@ -1047,7 +1007,7 @@ async function assertExactDatabaseCounts(db, scenario) {
|
||||
refunds: 0,
|
||||
late_reviews: 0,
|
||||
credit_transactions: 2,
|
||||
entitlement_grants: scenario.id === 'credits-3x' ? 1 : 0,
|
||||
entitlement_grants: 0,
|
||||
pack_entitlements: 0,
|
||||
matches: 0,
|
||||
match_entrants: 0,
|
||||
@@ -1090,32 +1050,18 @@ async function assertPaymentAndReceipt(db, context) {
|
||||
&& Number.isInteger(Number(row.address_index)) && Number(row.address_index) >= 0,
|
||||
'payment row lacks its immutable invoice destination identity');
|
||||
|
||||
if (context.scenario.id === 'direct-2x') {
|
||||
assert(row.product_id === 'single_game' && Number(row.credits_purchased) === 0
|
||||
&& Number(row.product_grants?.credits || 0) === 0
|
||||
&& Number(row.product_grants?.raceEntries || 0) === 0
|
||||
&& Array.isArray(row.product_grants?.packs) && row.product_grants.packs.length === 0
|
||||
&& row.product_grants?.premiumLevel == null,
|
||||
'direct payment product identity changed');
|
||||
assert(Number(row.fairness_proof_version) === 2 && row.bound && row.consumed,
|
||||
'direct payment fairness binding was not consumed exactly once');
|
||||
assert(row.fairness_offer_id === context.offer.offerId
|
||||
&& row.fairness_commitment === context.offer.commitment
|
||||
&& row.fairness_client_seed === context.clientSeed,
|
||||
'direct payment fairness identity changed');
|
||||
} else {
|
||||
assert(row.product_id === context.packageId
|
||||
&& Number(row.credits_purchased) === context.grantedCredits
|
||||
&& Number(row.product_grants?.credits) === context.grantedCredits
|
||||
&& Number(row.product_grants?.raceEntries || 0) === 0
|
||||
&& Array.isArray(row.product_grants?.packs) && row.product_grants.packs.length === 0
|
||||
&& row.product_grants?.premiumLevel == null,
|
||||
'credits payment product or durable grant identity changed');
|
||||
assert(row.fairness_proof_version == null
|
||||
&& row.fairness_offer_id == null
|
||||
&& !row.bound && !row.consumed,
|
||||
'credits package was incorrectly bound to a game fairness offer');
|
||||
}
|
||||
assert(row.product_id === 'single_game' && Number(row.credits_purchased) === 0
|
||||
&& Number(row.product_grants?.credits || 0) === 0
|
||||
&& Number(row.product_grants?.raceEntries || 0) === 0
|
||||
&& Array.isArray(row.product_grants?.packs) && row.product_grants.packs.length === 0
|
||||
&& row.product_grants?.premiumLevel == null,
|
||||
'direct payment product identity changed');
|
||||
assert(Number(row.fairness_proof_version) === 2 && row.bound && row.consumed,
|
||||
'direct payment fairness binding was not consumed exactly once');
|
||||
assert(row.fairness_offer_id === context.offer.offerId
|
||||
&& row.fairness_commitment === context.offer.commitment
|
||||
&& row.fairness_client_seed === context.clientSeed,
|
||||
'direct payment fairness identity changed');
|
||||
|
||||
const receipt = await db.query(`
|
||||
SELECT COUNT(*)::int AS count,
|
||||
@@ -1199,15 +1145,9 @@ async function assertGameAndPayout(db, context) {
|
||||
&& row.client_seed === context.clientSeed,
|
||||
'durable game fairness identity changed');
|
||||
|
||||
if (context.scenario.id === 'direct-2x') {
|
||||
assert(String(row.payment_id) === String(context.paymentId)
|
||||
&& row.entry_credits_spent == null,
|
||||
'direct game did not consume exactly its bound payment');
|
||||
} else {
|
||||
assert(row.payment_id == null
|
||||
&& Number(row.entry_credits_spent) === context.creditsPerGame,
|
||||
'credits game did not consume exactly its credit entry');
|
||||
}
|
||||
assert(String(row.payment_id) === String(context.paymentId)
|
||||
&& row.entry_credits_spent == null,
|
||||
'direct game did not consume exactly its bound payment');
|
||||
|
||||
assert(String(row.payout_id) === String(context.payoutId)
|
||||
&& BigInt(row.amount) === context.payoutAmount
|
||||
@@ -1238,62 +1178,21 @@ async function assertCreditLedger(db, context, userId) {
|
||||
`, [userId]);
|
||||
assert(ledger.rows.length === 2, 'canary user does not have exactly two credit-ledger rows');
|
||||
|
||||
if (context.scenario.id === 'direct-2x') {
|
||||
assert(Number(user.credits) === 0 && Number(user.total_credits_purchased) === 1,
|
||||
'direct entry did not net to zero and advance purchase progress once');
|
||||
const [purchase, spend] = ledger.rows;
|
||||
assert(Number(purchase.amount) === 1 && purchase.reason === 'direct_entry'
|
||||
&& purchase.transaction_type === 'purchase' && Number(purchase.balance_after) === 0
|
||||
&& purchase.payment_id == null,
|
||||
'direct-entry purchase ledger row is not exact');
|
||||
assert(Number(spend.amount) === -1 && spend.reason === 'game_entry'
|
||||
&& spend.transaction_type === 'spend' && Number(spend.balance_after) === 0
|
||||
&& spend.payment_id == null,
|
||||
'direct-entry spend ledger row is not exact');
|
||||
return;
|
||||
}
|
||||
|
||||
const finalBalance = context.grantedCredits - context.creditsPerGame;
|
||||
assert(Number(user.credits) === finalBalance
|
||||
&& Number(user.total_credits_purchased) === context.grantedCredits,
|
||||
'credits package grant or post-entry balance is wrong');
|
||||
assert(Number(user.credits) === 0 && Number(user.total_credits_purchased) === 1,
|
||||
'direct entry did not net to zero and advance purchase progress once');
|
||||
const [purchase, spend] = ledger.rows;
|
||||
assert(Number(purchase.amount) === context.grantedCredits
|
||||
&& purchase.reason === 'package_purchase'
|
||||
&& purchase.transaction_type === 'purchase'
|
||||
&& Number(purchase.balance_after) === context.grantedCredits
|
||||
&& String(purchase.payment_id) === String(context.paymentId),
|
||||
'package-purchase ledger row is not exact');
|
||||
assert(Number(spend.amount) === -context.creditsPerGame
|
||||
&& spend.reason === 'game_entry'
|
||||
&& spend.transaction_type === 'spend'
|
||||
&& Number(spend.balance_after) === finalBalance
|
||||
assert(Number(purchase.amount) === 1 && purchase.reason === 'direct_entry'
|
||||
&& purchase.transaction_type === 'purchase' && Number(purchase.balance_after) === 0
|
||||
&& purchase.payment_id == null,
|
||||
'direct-entry purchase ledger row is not exact');
|
||||
assert(Number(spend.amount) === -1 && spend.reason === 'game_entry'
|
||||
&& spend.transaction_type === 'spend' && Number(spend.balance_after) === 0
|
||||
&& spend.payment_id == null,
|
||||
'credits game-entry ledger row is not exact');
|
||||
|
||||
const grantResult = await db.query(`
|
||||
SELECT source, credits_granted, purchase_progress_granted,
|
||||
race_entries_granted, packs_granted, premium_level_granted,
|
||||
status, credits_reversed, purchase_progress_reversed,
|
||||
race_entries_reversed
|
||||
FROM payment_entitlement_grants WHERE payment_id = $1 AND user_id = $2
|
||||
`, [context.paymentId, userId]);
|
||||
const grant = grantResult.rows[0];
|
||||
assert(grantResult.rows.length === 1 && grant.source === 'product_confirmation'
|
||||
&& grant.status === 'active'
|
||||
&& Number(grant.credits_granted) === context.grantedCredits
|
||||
&& Number(grant.purchase_progress_granted) === context.grantedCredits
|
||||
&& Number(grant.race_entries_granted) === 0
|
||||
&& Array.isArray(grant.packs_granted) && grant.packs_granted.length === 0
|
||||
&& grant.premium_level_granted == null
|
||||
&& Number(grant.credits_reversed) === 0
|
||||
&& Number(grant.purchase_progress_reversed) === 0
|
||||
&& Number(grant.race_entries_reversed) === 0,
|
||||
'payment-scoped credits entitlement marker is not exact');
|
||||
'direct-entry spend ledger row is not exact');
|
||||
}
|
||||
|
||||
async function assertDatabaseSettlement(db, context) {
|
||||
await assertExactDatabaseCounts(db, context.scenario);
|
||||
await assertExactDatabaseCounts(db);
|
||||
const paymentUserId = await assertPaymentAndReceipt(db, context);
|
||||
const gameUserId = await assertGameAndPayout(db, context);
|
||||
assert(String(paymentUserId) === String(gameUserId),
|
||||
@@ -1302,7 +1201,7 @@ async function assertDatabaseSettlement(db, context) {
|
||||
phase('exact receipt, entitlement, credit, game, liability, and payout rows verified');
|
||||
}
|
||||
|
||||
function assertModeInfo(info, config) {
|
||||
function assertModeInfo(info) {
|
||||
assert(info?.operatedProductProfileId === OPERATED_PROFILE_ID
|
||||
&& info?.cryptoType === 'XMR'
|
||||
&& info.network === 'stagenet'
|
||||
@@ -1311,9 +1210,9 @@ function assertModeInfo(info, config) {
|
||||
&& info.gameName === 'Monerogue',
|
||||
'Socket.IO product identity is not Monerogue XMR stagenet');
|
||||
assert(info.paymentsEnabled && info.payoutsEnabled
|
||||
&& info.directModeEnabled && info.creditsModeEnabled
|
||||
&& info.directPayoutsEnabled && info.creditsPayoutsEnabled,
|
||||
'mixed direct/credits mode and both payout paths must be enabled');
|
||||
&& info.directModeEnabled && !info.creditsModeEnabled
|
||||
&& info.directPayoutsEnabled && !info.creditsPayoutsEnabled,
|
||||
'direct entry/payout must be enabled and purchased-credit paths must be disabled');
|
||||
assert(info.smirkEnabled === false, 'Smirk must be disabled on stagenet');
|
||||
assert(info.modes?.solo === true
|
||||
&& info.modes?.tavern === true
|
||||
@@ -1321,39 +1220,14 @@ function assertModeInfo(info, config) {
|
||||
&& JSON.stringify(enabledEconomyIds(info.modes?.match?.economies))
|
||||
=== JSON.stringify(['credits_prestige', 'free']),
|
||||
'operated Tavern/match modes or crypto-PvP exclusion drifted');
|
||||
for (const key of ['direct', 'credits']) {
|
||||
assert(Number(info.payoutMultipliers?.[key]?.escape) === 2
|
||||
&& Number(info.payoutMultipliers?.[key]?.escapeWithTreasure) === 3,
|
||||
`${key} Socket.IO payout multipliers are not exact 2x/3x`);
|
||||
}
|
||||
assert(Number(info.payoutMultipliers?.direct?.escape) === 2
|
||||
&& Number(info.payoutMultipliers?.direct?.escapeWithTreasure) === 3,
|
||||
'direct Socket.IO payout multipliers are not exact 2x/3x');
|
||||
assert(/^\d+$/.test(String(info.singleGamePrice))
|
||||
&& BigInt(String(info.singleGamePrice)) > 0n
|
||||
&& BigInt(String(info.singleGamePrice)) * 3n <= BigInt(Number.MAX_SAFE_INTEGER),
|
||||
'Socket.IO direct price or its 3x result is not a safe positive atomic value');
|
||||
|
||||
const selected = (info.creditPackages || []).find(item => item.id === config.packageId);
|
||||
assert(selected, 'selected credits package is not advertised');
|
||||
const grantedCredits = Number(selected.grants?.credits);
|
||||
const creditsPerGame = Number(info.creditsPerGame);
|
||||
assert(Number.isSafeInteger(grantedCredits) && Number.isSafeInteger(creditsPerGame)
|
||||
&& grantedCredits >= creditsPerGame && creditsPerGame > 0,
|
||||
'selected package cannot fund exactly one credits game');
|
||||
assert(Number(selected.grants?.raceEntries || 0) === 0
|
||||
&& Array.isArray(selected.grants?.packs) && selected.grants.packs.length === 0
|
||||
&& selected.grants?.premiumLevel == null,
|
||||
'financial canary requires a pure credits-only package');
|
||||
assert(/^\d+$/.test(String(selected.price)) && BigInt(String(selected.price)) > 0n,
|
||||
'selected package has no positive atomic price');
|
||||
assert(/^\d+$/.test(String(info.creditsPayoutBaseValue))
|
||||
&& BigInt(String(info.creditsPayoutBaseValue)) > 0n
|
||||
&& BigInt(String(info.creditsPayoutBaseValue)) * 3n <= BigInt(Number.MAX_SAFE_INTEGER),
|
||||
'credits payout base or its 3x result is not a safe positive atomic value');
|
||||
return Object.freeze({
|
||||
...selected,
|
||||
grantedCredits,
|
||||
creditsPerGame,
|
||||
payoutBase: BigInt(String(info.creditsPayoutBaseValue))
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForPaymentConfirmation(journal, paymentId, timeoutMs) {
|
||||
@@ -1368,29 +1242,6 @@ async function waitForPaymentConfirmation(journal, paymentId, timeoutMs) {
|
||||
return outcome.data || {};
|
||||
}
|
||||
|
||||
async function startCreditsGame(socket, journal, acknowledgement) {
|
||||
const offer = await requestFreshFairness(socket, journal);
|
||||
const clientSeed = crypto.randomBytes(32).toString('hex');
|
||||
const after = journal.seq;
|
||||
socket.emit('auto_start', {
|
||||
fairnessOfferId: offer.offerId,
|
||||
clientSeed,
|
||||
legalAcknowledgement: acknowledgement
|
||||
});
|
||||
const outcome = await journal.waitAny([
|
||||
{ name: 'game_start' },
|
||||
{ name: 'fairness_error' },
|
||||
{ name: 'status_update', test: data => data?.type === 'error' || data?.status === 'error' },
|
||||
{ name: '$disconnect' }
|
||||
], 60000, after);
|
||||
assert(outcome.name === 'game_start', `credits game did not start (${outcome.name})`);
|
||||
assert(outcome.data?.proof?.offerId === offer.offerId
|
||||
&& outcome.data?.proof?.commitment === offer.commitment
|
||||
&& outcome.data?.proof?.clientSeed === clientSeed,
|
||||
'credits game did not consume the fresh fairness offer');
|
||||
return { gameStart: outcome, offer, clientSeed };
|
||||
}
|
||||
|
||||
async function runLive(config, modules, db, legal, env = process.env) {
|
||||
assertLiveSafety(config, env);
|
||||
const funding = createFundingRpc(modules, env);
|
||||
@@ -1416,7 +1267,7 @@ async function runLive(config, modules, db, legal, env = process.env) {
|
||||
assert(token.length >= 32, 'anonymous canary session token was not issued');
|
||||
const modeEvent = await journal.waitAny([{ name: 'game_mode_info' }], 15000);
|
||||
const modeInfo = modeEvent.data || {};
|
||||
const packageInfo = assertModeInfo(modeInfo, config);
|
||||
assertModeInfo(modeInfo);
|
||||
|
||||
// Refetch immediately before the value-bearing action so the echoed version is current.
|
||||
const currentLegal = await fetchCanonicalAcknowledgement(config);
|
||||
@@ -1425,60 +1276,29 @@ async function runLive(config, modules, db, legal, env = process.env) {
|
||||
const acknowledgement = currentLegal.acknowledgement;
|
||||
await savePayoutAddress(socket, journal, fundingIdentity.address);
|
||||
|
||||
let context;
|
||||
if (config.scenario.id === 'direct-2x') {
|
||||
const direct = await createDirectInvoice(socket, journal, acknowledgement, config, modeInfo);
|
||||
context = {
|
||||
scenario: config.scenario,
|
||||
paymentId: direct.invoice.paymentId,
|
||||
invoiceAddress: direct.invoice.address,
|
||||
payoutAddress: fundingIdentity.address,
|
||||
entryAmount: direct.amount,
|
||||
payoutBase: direct.amount,
|
||||
offer: direct.offer,
|
||||
clientSeed: direct.clientSeed
|
||||
};
|
||||
await assertSeparateFundingWallet(funding, direct.invoice.address);
|
||||
await sendExactEntry(funding, direct.invoice.address, direct.amount, config, transferGate);
|
||||
await waitForPaymentConfirmation(journal, direct.invoice.paymentId, config.paymentTimeoutMs);
|
||||
const gameStart = await journal.waitAny([
|
||||
{ name: 'game_start' }, { name: '$disconnect' }
|
||||
], 60000);
|
||||
assert(gameStart.name === 'game_start', 'direct paid game did not start');
|
||||
assert(gameStart.data?.proof?.offerId === direct.offer.offerId
|
||||
&& gameStart.data?.proof?.commitment === direct.offer.commitment
|
||||
&& gameStart.data?.proof?.clientSeed === direct.clientSeed,
|
||||
'started direct dungeon is not bound to its invoice fairness proof');
|
||||
context.gameStart = gameStart;
|
||||
} else {
|
||||
const purchase = await createCreditsInvoice(socket, journal, acknowledgement,
|
||||
config, packageInfo);
|
||||
context = {
|
||||
scenario: config.scenario,
|
||||
paymentId: purchase.invoice.paymentId,
|
||||
invoiceAddress: purchase.invoice.address,
|
||||
payoutAddress: fundingIdentity.address,
|
||||
entryAmount: purchase.amount,
|
||||
payoutBase: purchase.payoutBase,
|
||||
packageId: config.packageId,
|
||||
grantedCredits: purchase.grantedCredits,
|
||||
creditsPerGame: purchase.creditsPerGame
|
||||
};
|
||||
await assertSeparateFundingWallet(funding, purchase.invoice.address);
|
||||
await sendExactEntry(funding, purchase.invoice.address, purchase.amount, config, transferGate);
|
||||
const confirmation = await waitForPaymentConfirmation(journal,
|
||||
purchase.invoice.paymentId, config.paymentTimeoutMs);
|
||||
assert(Number(confirmation.creditsAdded) === purchase.grantedCredits
|
||||
&& Number(confirmation.newBalance) === purchase.grantedCredits,
|
||||
'credits confirmation did not grant the exact advertised balance');
|
||||
const creditUpdate = journal.latest('credits_update', data =>
|
||||
Number(data?.balance) === purchase.grantedCredits);
|
||||
assert(creditUpdate, 'credits balance update was not observed after package confirmation');
|
||||
const started = await startCreditsGame(socket, journal, acknowledgement);
|
||||
context.offer = started.offer;
|
||||
context.clientSeed = started.clientSeed;
|
||||
context.gameStart = started.gameStart;
|
||||
}
|
||||
const direct = await createDirectInvoice(socket, journal, acknowledgement, config, modeInfo);
|
||||
const context = {
|
||||
scenario: config.scenario,
|
||||
paymentId: direct.invoice.paymentId,
|
||||
invoiceAddress: direct.invoice.address,
|
||||
payoutAddress: fundingIdentity.address,
|
||||
entryAmount: direct.amount,
|
||||
payoutBase: direct.amount,
|
||||
offer: direct.offer,
|
||||
clientSeed: direct.clientSeed
|
||||
};
|
||||
await assertSeparateFundingWallet(funding, direct.invoice.address);
|
||||
await sendExactEntry(funding, direct.invoice.address, direct.amount, config, transferGate);
|
||||
await waitForPaymentConfirmation(journal, direct.invoice.paymentId, config.paymentTimeoutMs);
|
||||
const gameStart = await journal.waitAny([
|
||||
{ name: 'game_start' }, { name: '$disconnect' }
|
||||
], 60000);
|
||||
assert(gameStart.name === 'game_start', 'direct paid game did not start');
|
||||
assert(gameStart.data?.proof?.offerId === direct.offer.offerId
|
||||
&& gameStart.data?.proof?.commitment === direct.offer.commitment
|
||||
&& gameStart.data?.proof?.clientSeed === direct.clientSeed,
|
||||
'started direct dungeon is not bound to its invoice fairness proof');
|
||||
context.gameStart = gameStart;
|
||||
|
||||
const gameOver = await runBoundedBot(socket, journal, context.gameStart,
|
||||
config.scenario, config);
|
||||
@@ -1496,10 +1316,8 @@ async function runLive(config, modules, db, legal, env = process.env) {
|
||||
&& paymentRow.type === config.scenario.paymentType
|
||||
&& BigInt(String(paymentRow.amount)) === context.entryAmount,
|
||||
'owned payment history differs from the confirmed invoice');
|
||||
if (config.scenario.id === 'credits-3x') {
|
||||
assert(Number(paymentRow.creditsReceived) === context.grantedCredits,
|
||||
'owned payment history has the wrong credit grant');
|
||||
}
|
||||
assert(Number(paymentRow.creditsReceived || 0) === 0,
|
||||
'owned payment history unexpectedly reports a purchased-credit grant');
|
||||
|
||||
const payoutRow = await waitForCompletedPayout(config, socket.id, token, completion);
|
||||
await waitForIncomingPayout(funding, payoutRow.txHash, completion.expected, config);
|
||||
@@ -1509,14 +1327,8 @@ async function runLive(config, modules, db, legal, env = process.env) {
|
||||
headers: { 'X-Session-Token': token }
|
||||
});
|
||||
assert(credits.response.status === 200, 'owned credits endpoint is not HTTP 200');
|
||||
const expectedCredits = config.scenario.id === 'direct-2x'
|
||||
? 0
|
||||
: context.grantedCredits - context.creditsPerGame;
|
||||
const expectedProgress = config.scenario.id === 'direct-2x'
|
||||
? 1
|
||||
: context.grantedCredits;
|
||||
assert(Number(credits.body?.credits) === expectedCredits
|
||||
&& Number(credits.body?.totalCreditsPurchased) === expectedProgress,
|
||||
assert(Number(credits.body?.credits) === 0
|
||||
&& Number(credits.body?.totalCreditsPurchased) === 1,
|
||||
'owned credits endpoint differs from the exact scenario ledger');
|
||||
|
||||
await assertDatabaseSettlement(db, context);
|
||||
@@ -1529,7 +1341,7 @@ async function runLive(config, modules, db, legal, env = process.env) {
|
||||
function printHelp() {
|
||||
process.stdout.write(`Monerogue XMR stagenet financial canary\n\n`
|
||||
+ `Modes: preflight | database-preflight | live-stagenet\n`
|
||||
+ `Scenarios: direct-2x | credits-3x\n\n`
|
||||
+ `Scenarios: direct-2x | direct-3x\n\n`
|
||||
+ `This harness is localhost-only, stagenet-only, fresh-database-only, and one-transfer-only.\n`
|
||||
+ `Read ../docs/STAGENET_FINANCIAL_CANARY.md before live use.\n`);
|
||||
}
|
||||
|
||||
@@ -52,9 +52,10 @@ function leaderboardMapping(disclosure) {
|
||||
}
|
||||
if (profileId === OPERATED_PRODUCT_PROFILE_IDS.XMR_STAGENET) {
|
||||
return 'FREE solo runs and free competitive matches use the Pleb board. '
|
||||
+ 'PAID_SINGLE and PAID_CREDITS solo runs use the Hall of Champions; match-generated '
|
||||
+ 'game rows are excluded. credits_prestige matches use the separate Prestige board. '
|
||||
+ 'This operated profile does not offer crypto_race.';
|
||||
+ 'Only PAID_SINGLE solo runs use the Hall of Champions; historical PAID_CREDITS '
|
||||
+ 'runs and match-generated game rows are excluded. credits_prestige matches use the '
|
||||
+ 'separate Prestige board. This operated profile does not sell credits and does not '
|
||||
+ 'offer crypto_race.';
|
||||
}
|
||||
return 'Solo scores map by entry: FREE runs use the Pleb board, while PAID_SINGLE and '
|
||||
+ 'PAID_CREDITS runs use the Hall of Champions. PvP maps separately by economy: free '
|
||||
|
||||
@@ -86,13 +86,17 @@ describe('commerce disclosure and paid-action policy', () => {
|
||||
});
|
||||
|
||||
test('publishes conspicuous no-real-value monerogue.app scope and MIT operator boundary', () => {
|
||||
const disclosure = buildCommerceDisclosure(manager(), env({
|
||||
const disclosure = buildCommerceDisclosure(manager({
|
||||
creditsModeEnabled: false
|
||||
}), env({
|
||||
OPERATED_PRODUCT_PROFILE: 'such-monerogue-stagenet'
|
||||
}));
|
||||
|
||||
expect(disclosure.operatedProduct.scopeNotice).toContain('single-player 2×/3× outcomes are test gambling mechanics');
|
||||
expect(disclosure.operatedProduct.noRealValueNotice).toContain('NO REAL VALUE');
|
||||
expect(disclosure.operatedProduct.commerceSummary).toContain('never send mainnet XMR');
|
||||
expect(disclosure.operatedProduct.commerceSummary).toMatch(/never send mainnet XMR/i);
|
||||
expect(disclosure.operatedProduct.commerceSummary).toContain('Purchased-credit entry');
|
||||
expect(disclosure.service.paidCreditsEnabled).toBe(false);
|
||||
expect(disclosure.service.cryptoMatchPayoutsEnabled).toBe(false);
|
||||
expect(disclosure.software).toEqual(expect.objectContaining({
|
||||
license: 'MIT',
|
||||
|
||||
@@ -225,7 +225,7 @@ describe('EnvironmentValidator production launch gate', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('accepts the exact Such Software monerogue.app stagenet 2x/3x contract', () => {
|
||||
test('accepts the exact direct-only Such Software monerogue.app stagenet 2x/3x contract', () => {
|
||||
const env = productionEnv({
|
||||
OPERATED_PRODUCT_PROFILE: 'such-monerogue-stagenet',
|
||||
HOSTED_BY: 'https://monerogue.app',
|
||||
@@ -233,16 +233,14 @@ describe('EnvironmentValidator production launch gate', () => {
|
||||
OPERATOR_CONTACT_URL: 'mailto:apps@such.software',
|
||||
OPERATOR_CONTACT_LABEL: 'apps@such.software',
|
||||
FREE_PLAY_ENABLED: 'true',
|
||||
PAYMENT_MODES: 'direct,credits',
|
||||
PAYMENT_MODES: 'direct',
|
||||
DIRECT_PAYMENT_ENABLED: 'true',
|
||||
CREDITS_ENABLED: 'true',
|
||||
CREDITS_ENABLED: 'false',
|
||||
PAYOUTS_ENABLED: 'true',
|
||||
DIRECT_PAYOUTS_ENABLED: 'true',
|
||||
CREDITS_PAYOUTS_ENABLED: 'true',
|
||||
CREDITS_PAYOUTS_ENABLED: 'false',
|
||||
DIRECT_PAYOUT_ESCAPE: '2.0',
|
||||
DIRECT_PAYOUT_TREASURE: '3.0',
|
||||
CREDITS_PAYOUT_ESCAPE: '2.0',
|
||||
CREDITS_PAYOUT_TREASURE: '3.0',
|
||||
ALLOW_MAINNET_PAYOUTS: 'false',
|
||||
SOLO_ENABLED: 'true',
|
||||
TAVERN_ENABLED: 'true',
|
||||
@@ -251,11 +249,18 @@ describe('EnvironmentValidator production launch gate', () => {
|
||||
MATCH_PAYOUTS_ENABLED: 'false'
|
||||
});
|
||||
const operatedConfig = config({
|
||||
modes: {
|
||||
direct: { enabled: true, price: 10000n },
|
||||
credits: {
|
||||
enabled: false,
|
||||
packages: [{ id: 'small', credits: 10, price: 90000000000n }]
|
||||
}
|
||||
},
|
||||
payouts: {
|
||||
enabled: true,
|
||||
rules: {
|
||||
direct: { enabled: true },
|
||||
credits: { enabled: true }
|
||||
credits: { enabled: false }
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -365,6 +370,9 @@ describe('EnvironmentValidator production launch gate', () => {
|
||||
expect.stringContaining('requires OPERATOR_NAME=Such Software'),
|
||||
expect.stringContaining('requires OPERATOR_CONTACT_URL=mailto:apps@such.software'),
|
||||
expect.stringContaining('requires MONERO_NETWORK=stagenet'),
|
||||
expect.stringContaining('requires PAYMENT_MODES=direct'),
|
||||
expect.stringContaining('requires CREDITS_ENABLED=false'),
|
||||
expect.stringContaining('requires CREDITS_PAYOUTS_ENABLED=false'),
|
||||
expect.stringContaining('requires DIRECT_PAYOUT_ESCAPE=2'),
|
||||
expect.stringContaining('requires SOLO_ENABLED=true'),
|
||||
expect.stringContaining('requires TAVERN_ENABLED=true'),
|
||||
|
||||
@@ -120,16 +120,16 @@ describe('GET /api/leaderboard operated-product integrity', () => {
|
||||
expect(db.calls[0].params.slice(1, 3)).toEqual([['PAID_CREDITS'], true]);
|
||||
});
|
||||
|
||||
test('Monerogue Champions includes direct and credit solo, never match-generated rows', async () => {
|
||||
test('Monerogue Champions includes direct solo only, never historical credit or match rows', async () => {
|
||||
const { response, db } = await invokeLeaderboardRoute('such-monerogue-stagenet', {
|
||||
board: 'champions',
|
||||
period: 'all'
|
||||
});
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.leaderboard.map(row => row.name))
|
||||
.toEqual(['CreditSolo', 'LegacyDirect']);
|
||||
.toEqual(['LegacyDirect']);
|
||||
expect(db.calls[0].params.slice(1, 3))
|
||||
.toEqual([['PAID_SINGLE', 'PAID_CREDITS'], true]);
|
||||
.toEqual([['PAID_SINGLE'], true]);
|
||||
});
|
||||
|
||||
test('independent deployments retain the generic opt-in crypto-match mapping', async () => {
|
||||
|
||||
@@ -82,6 +82,7 @@ describe('production disclosure pages', () => {
|
||||
currencyLabel: 'sXMR',
|
||||
network: 'stagenet',
|
||||
isTestNetwork: true,
|
||||
creditsModeEnabled: false,
|
||||
payoutsEnabled: true,
|
||||
isPayoutEnabledForMode: () => true
|
||||
}));
|
||||
@@ -98,11 +99,13 @@ describe('production disclosure pages', () => {
|
||||
currencyLabel: 'sXMR',
|
||||
network: 'stagenet',
|
||||
isTestNetwork: true,
|
||||
creditsModeEnabled: false,
|
||||
payoutsEnabled: true,
|
||||
isPayoutEnabledForMode: () => true
|
||||
}));
|
||||
expect(terms).toContain('PAID_SINGLE and PAID_CREDITS solo runs use the Hall of Champions');
|
||||
expect(terms).toContain('match-generated game rows are excluded');
|
||||
expect(terms).toContain('Only PAID_SINGLE solo runs use the Hall of Champions');
|
||||
expect(terms).toContain('historical PAID_CREDITS runs and match-generated game rows are excluded');
|
||||
expect(terms).toContain('does not sell credits');
|
||||
expect(terms).toContain('does not offer crypto_race');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -62,7 +62,7 @@ describe('operated product environment templates', () => {
|
||||
}));
|
||||
});
|
||||
|
||||
test('monerogue.app is exactly stagenet 2x/3x solo testing with crypto-match payouts off', () => {
|
||||
test('monerogue.app is exactly direct-only stagenet 2x/3x solo testing', () => {
|
||||
const env = parseExample('.env.stagenet.example');
|
||||
|
||||
expect(env).toEqual(expect.objectContaining({
|
||||
@@ -78,14 +78,14 @@ describe('operated product environment templates', () => {
|
||||
CRYPTO_TYPE: 'XMR',
|
||||
MONERO_NETWORK: 'stagenet',
|
||||
FREE_PLAY_ENABLED: 'true',
|
||||
PAYMENT_MODES: 'direct,credits',
|
||||
PAYMENT_MODES: 'direct',
|
||||
DIRECT_PAYMENT_ENABLED: 'true',
|
||||
CREDITS_ENABLED: 'false',
|
||||
PAYOUTS_ENABLED: 'true',
|
||||
DIRECT_PAYOUTS_ENABLED: 'true',
|
||||
CREDITS_PAYOUTS_ENABLED: 'true',
|
||||
CREDITS_PAYOUTS_ENABLED: 'false',
|
||||
DIRECT_PAYOUT_ESCAPE: '2.0',
|
||||
DIRECT_PAYOUT_TREASURE: '3.0',
|
||||
CREDITS_PAYOUT_ESCAPE: '2.0',
|
||||
CREDITS_PAYOUT_TREASURE: '3.0',
|
||||
ALLOW_MAINNET_PAYOUTS: 'false',
|
||||
SOLO_ENABLED: 'true',
|
||||
TAVERN_ENABLED: 'true',
|
||||
@@ -96,9 +96,6 @@ describe('operated product environment templates', () => {
|
||||
expect(BigInt(env.PAYOUT_MAX_PER_GAME)).toBeGreaterThanOrEqual(
|
||||
3n * BigInt(env.DIRECT_GAME_PRICE)
|
||||
);
|
||||
expect(BigInt(env.PAYOUT_MAX_PER_GAME)).toBeGreaterThanOrEqual(
|
||||
3n * BigInt(env.CREDITS_PAYOUT_BASE)
|
||||
);
|
||||
expect(env.COSMETIC_PRODUCTS).toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -109,7 +106,7 @@ describe('operated product environment templates', () => {
|
||||
|
||||
test.each([
|
||||
['.env.mainnet.example', 'paid modes: credits'],
|
||||
['.env.stagenet.example', 'paid modes: direct, credits']
|
||||
['.env.stagenet.example', 'paid modes: direct']
|
||||
])('%s passes the real production preflight after only secret substitution', (name, summary) => {
|
||||
const result = preflightExample(name);
|
||||
expect({ status: result.status, stderr: result.stderr }).toEqual({ status: 0, stderr: '' });
|
||||
|
||||
@@ -43,7 +43,7 @@ describe('prestige leaderboard periods', () => {
|
||||
excludeMatchGenerated: true
|
||||
});
|
||||
expect(soloLeaderboardPolicy('such-monerogue-stagenet', 'champions')).toEqual({
|
||||
gameModes: ['PAID_SINGLE', 'PAID_CREDITS'],
|
||||
gameModes: ['PAID_SINGLE'],
|
||||
excludeMatchGenerated: true
|
||||
});
|
||||
expect(soloLeaderboardPolicy(null, 'champions')).toEqual({
|
||||
|
||||
@@ -62,21 +62,21 @@ function visibleState({ player, exit, treasure }) {
|
||||
|
||||
describe('XMR stagenet financial canary safety contract', () => {
|
||||
test('recognizes only the two explicit financial scenarios', () => {
|
||||
expect(Object.keys(SCENARIOS)).toEqual(['direct-2x', 'credits-3x']);
|
||||
expect(Object.keys(SCENARIOS)).toEqual(['direct-2x', 'direct-3x']);
|
||||
expect(SCENARIOS['direct-2x']).toEqual(expect.objectContaining({
|
||||
paymentType: 'single_game',
|
||||
gameMode: 'PAID_SINGLE',
|
||||
multiplier: 2,
|
||||
collectTreasure: false
|
||||
}));
|
||||
expect(SCENARIOS['credits-3x']).toEqual(expect.objectContaining({
|
||||
paymentType: 'credits_package',
|
||||
gameMode: 'PAID_CREDITS',
|
||||
expect(SCENARIOS['direct-3x']).toEqual(expect.objectContaining({
|
||||
paymentType: 'single_game',
|
||||
gameMode: 'PAID_SINGLE',
|
||||
multiplier: 3,
|
||||
collectTreasure: true
|
||||
}));
|
||||
expect(() => readConfiguration(configEnv({ E2E_SCENARIO: 'anything-else' })))
|
||||
.toThrow(/direct-2x or credits-3x/);
|
||||
.toThrow(/direct-2x or direct-3x/);
|
||||
});
|
||||
|
||||
test('refuses public targets, embedded credentials, paths, and the wrong port', () => {
|
||||
@@ -96,17 +96,17 @@ describe('XMR stagenet financial canary safety contract', () => {
|
||||
|
||||
const env = configEnv({
|
||||
E2E_MODE: 'live-stagenet',
|
||||
E2E_SCENARIO: 'credits-3x',
|
||||
E2E_SCENARIO: 'direct-3x',
|
||||
E2E_MAX_TRANSFER_ATOMIC: '500000000000',
|
||||
E2E_CONFIRM: LIVE_CONFIRM,
|
||||
E2E_SCENARIO_CONFIRM: SCENARIOS['credits-3x'].scenarioConfirm,
|
||||
E2E_SCENARIO_CONFIRM: SCENARIOS['direct-3x'].scenarioConfirm,
|
||||
E2E_CANARY_PROFILE: SAFE_PROFILE_CONFIRM,
|
||||
E2E_DATABASE_NONCE_FILE: '/run/credentials/canary/database-nonce'
|
||||
});
|
||||
const config = readConfiguration(env);
|
||||
expect(() => assertLiveSafety(config, env)).not.toThrow();
|
||||
expect(() => assertLiveSafety(config, { ...env, E2E_SCENARIO_CONFIRM: 'DIRECT_2X_ESCAPE' }))
|
||||
.toThrow(/CREDITS_PACKAGE_THEN_3X/);
|
||||
.toThrow(/DIRECT_3X_TREASURE_ESCAPE/);
|
||||
expect(() => assertLiveSafety(config, { ...env, E2E_CONFIRM: '' }))
|
||||
.toThrow(/E2E_CONFIRM/);
|
||||
const noNonceConfig = readConfiguration({ ...env, E2E_DATABASE_NONCE_FILE: '' });
|
||||
@@ -131,7 +131,7 @@ describe('XMR stagenet financial canary safety contract', () => {
|
||||
isTestNetwork: true,
|
||||
paymentsEnabled: true,
|
||||
directPaidEntryEnabled: true,
|
||||
paidCreditsEnabled: true,
|
||||
paidCreditsEnabled: false,
|
||||
soloPayoutsEnabled: true,
|
||||
anyPayoutsEnabled: true,
|
||||
paidPrestigeOnly: false,
|
||||
@@ -151,7 +151,7 @@ describe('XMR stagenet financial canary safety contract', () => {
|
||||
expect(() => canonicalAcknowledgement({
|
||||
...disclosure,
|
||||
service: { ...disclosure.service, cryptoMatchPayoutsEnabled: true }
|
||||
})).toThrow(/payout-enabled paid solo modes/);
|
||||
})).toThrow(/direct-only payout-enabled solo play/);
|
||||
expect(() => canonicalAcknowledgement({
|
||||
...disclosure,
|
||||
operatedProduct: { ...disclosure.operatedProduct, id: 'unreviewed-profile' }
|
||||
@@ -166,7 +166,7 @@ describe('XMR stagenet financial canary safety contract', () => {
|
||||
isTestNetwork: false,
|
||||
paymentsEnabled: true,
|
||||
directPaidEntryEnabled: true,
|
||||
paidCreditsEnabled: true,
|
||||
paidCreditsEnabled: false,
|
||||
soloPayoutsEnabled: true,
|
||||
anyPayoutsEnabled: true,
|
||||
paidPrestigeOnly: false,
|
||||
@@ -186,8 +186,8 @@ describe('XMR stagenet financial canary safety contract', () => {
|
||||
payoutMultiplier: { escape: 2, escapeWithTreasure: 3 }
|
||||
},
|
||||
PAID_CREDITS: {
|
||||
enabled: true,
|
||||
payoutMultiplier: { escape: 2, escapeWithTreasure: 3 }
|
||||
enabled: false,
|
||||
payoutMultiplier: 0
|
||||
},
|
||||
match: {
|
||||
enabled: true,
|
||||
@@ -237,6 +237,13 @@ describe('XMR stagenet financial canary safety contract', () => {
|
||||
...modes,
|
||||
PAID_CREDITS: {
|
||||
...modes.PAID_CREDITS,
|
||||
enabled: true
|
||||
}
|
||||
})).toThrow(/purchased-credit solo/);
|
||||
expect(() => assertPublicModeContract({
|
||||
...modes,
|
||||
PAID_SINGLE: {
|
||||
...modes.PAID_SINGLE,
|
||||
payoutMultiplier: { escape: 2, escapeWithTreasure: 4 }
|
||||
}
|
||||
})).toThrow(/not exactly 3x/);
|
||||
@@ -252,12 +259,12 @@ describe('XMR stagenet financial canary safety contract', () => {
|
||||
'monerogue_canary_direct_e2e', SCENARIOS['direct-2x']
|
||||
)).not.toThrow();
|
||||
expect(() => validateExpectedDatabaseName(
|
||||
'monerogue_canary_credits_e2e', SCENARIOS['credits-3x']
|
||||
'monerogue_canary_treasure_e2e', SCENARIOS['direct-3x']
|
||||
)).not.toThrow();
|
||||
expect(() => validateExpectedDatabaseName('monerogue', SCENARIOS['direct-2x']))
|
||||
.toThrow(/canary and e2e/);
|
||||
expect(() => validateExpectedDatabaseName(
|
||||
'monerogue_canary_credits_e2e', SCENARIOS['direct-2x']
|
||||
'monerogue_canary_treasure_e2e', SCENARIOS['direct-2x']
|
||||
)).toThrow(/scenario tag/);
|
||||
});
|
||||
|
||||
@@ -358,7 +365,7 @@ describe('XMR stagenet financial canary safety contract', () => {
|
||||
.toThrow(/could not prove/);
|
||||
});
|
||||
|
||||
test('direct bot avoids treasure while credits bot gets treasure before exit', () => {
|
||||
test('2x direct bot avoids treasure while 3x direct bot gets treasure before exit', () => {
|
||||
const directState = visibleState({
|
||||
player: { x: 0, y: 0, hasTreasure: false },
|
||||
treasure: [1, 0],
|
||||
@@ -369,28 +376,28 @@ describe('XMR stagenet financial canary safety contract', () => {
|
||||
expect(chooseMove(directKnown, directState, botState(), SCENARIOS['direct-2x']))
|
||||
.toBe('down');
|
||||
|
||||
const creditsState = visibleState({
|
||||
const treasureState = visibleState({
|
||||
player: { x: 0, y: 0, hasTreasure: false },
|
||||
treasure: [1, 0],
|
||||
exit: [2, 0]
|
||||
});
|
||||
const creditsKnown = new Map();
|
||||
addVisible(creditsKnown, creditsState);
|
||||
const creditsBot = botState();
|
||||
expect(chooseMove(creditsKnown, creditsState, creditsBot, SCENARIOS['credits-3x']))
|
||||
const treasureKnown = new Map();
|
||||
addVisible(treasureKnown, treasureState);
|
||||
const treasureBot = botState();
|
||||
expect(chooseMove(treasureKnown, treasureState, treasureBot, SCENARIOS['direct-3x']))
|
||||
.toBe('right');
|
||||
const collected = {
|
||||
...creditsState,
|
||||
...treasureState,
|
||||
player: { x: 1, y: 0, hasTreasure: true },
|
||||
treasure: null
|
||||
};
|
||||
expect(chooseMove(creditsKnown, collected, creditsBot, SCENARIOS['credits-3x']))
|
||||
expect(chooseMove(treasureKnown, collected, treasureBot, SCENARIOS['direct-3x']))
|
||||
.toBe('right');
|
||||
});
|
||||
|
||||
test.each([
|
||||
['direct-2x', 10n, false],
|
||||
['credits-3x', 7n, true]
|
||||
['direct-3x', 7n, true]
|
||||
])('verifies exact game-over liability and two-party proof for %s', (scenarioName, base, treasure) => {
|
||||
const scenario = SCENARIOS[scenarioName];
|
||||
const serverSeed = '11'.repeat(32);
|
||||
@@ -452,14 +459,14 @@ describe('XMR stagenet financial canary safety contract', () => {
|
||||
expect(safe).not.toContain('password@example');
|
||||
});
|
||||
|
||||
test('source has one non-retriable relay call, a guarded main, and both exact flows', () => {
|
||||
test('source has one non-retriable relay call, a guarded main, and direct-only entry', () => {
|
||||
const source = fs.readFileSync(SCRIPT, 'utf8');
|
||||
expect((source.match(/funding\.rpc\('transfer'/g) || [])).toHaveLength(1);
|
||||
expect(source).toMatch(/transferGate\.attempted = true;[\s\S]*do_not_relay: false/);
|
||||
expect(source).toContain('if (require.main === module)');
|
||||
expect(source).toContain("type: 'single_game'");
|
||||
expect(source).toContain("type: 'credits_package'");
|
||||
expect(source).toContain("socket.emit('auto_start'");
|
||||
expect(source).not.toContain("type: 'credits_package'");
|
||||
expect(source).not.toContain("socket.emit('auto_start'");
|
||||
expect(source).toContain('legalAcknowledgement: acknowledgement');
|
||||
expect(source).not.toContain('console.log(');
|
||||
const mainBody = source.slice(source.indexOf('async function main('));
|
||||
|
||||
Reference in New Issue
Block a user