Files
wownerogue/test/stagenetFinancialCanary.test.js
jwinterm fad2a695bd Wownerogue
A browser-based roguelike synchronized with crypto block timing. A run is a race
against the next block: reach the exit before the block lands, or the house wins.
The same codebase runs the solo dungeon, the social Tavern, and operator-selected
PvP and race rulesets, with separate free and paid leaderboards.

Self-hostable under the MIT License. See README.md to run it, docs/ARCHITECTURE.md
for the design, and docs/DEPLOY.md to operate an instance.
2026-07-27 08:23:01 -04:00

488 lines
19 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const {
LIVE_CONFIRM,
OPERATED_PROFILE_ID,
REQUIRED_MIGRATIONS,
SAFE_PROFILE_CONFIRM,
SCENARIOS,
addVisible,
assertEmptyFinancialDatabase,
assertInvoiceNotOwned,
assertLiveSafety,
assertPublicModeContract,
canonicalAcknowledgement,
chooseMove,
enabledEconomyIds,
readConfiguration,
safeMessage,
validateExpectedDatabaseName,
verifyCanaryDatabaseHandshake,
verifyGameOver
} = require('../src/scripts/stagenet-financial-canary');
const {
CANARY_HANDSHAKE_PROTOCOL,
computeProof
} = require('../src/services/canaryDatabaseIdentity');
const SCRIPT = path.join(__dirname, '../src/scripts/stagenet-financial-canary.js');
const RUNBOOK = path.join(__dirname, '../docs/STAGENET_FINANCIAL_CANARY.md');
function configEnv(overrides = {}) {
return {
E2E_MODE: 'preflight',
E2E_TARGET: 'http://127.0.0.1:3102',
...overrides
};
}
function botState() {
return {
goal: null,
objective: null,
frontiersTried: new Set()
};
}
function visibleState({ player, exit, treasure }) {
return {
player,
exit,
treasure,
monster: null,
visibleTiles: {
0: { 0: "'1", 1: "'1", 2: "'1" },
1: { 0: "'1", 1: "'1", 2: "'1" }
}
};
}
describe('XMR stagenet financial canary safety contract', () => {
test('recognizes only the two explicit financial scenarios', () => {
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['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 direct-3x/);
});
test('refuses public targets, embedded credentials, paths, and the wrong port', () => {
expect(() => readConfiguration(configEnv({ E2E_TARGET: 'https://example.test:3102' })))
.toThrow(/localhost/);
expect(() => readConfiguration(configEnv({ E2E_TARGET: 'http://user:pass@127.0.0.1:3102' })))
.toThrow(/credentials/);
expect(() => readConfiguration(configEnv({ E2E_TARGET: 'http://127.0.0.1:3000' })))
.toThrow(/3102/);
expect(() => readConfiguration(configEnv({ E2E_TARGET: 'http://127.0.0.1:3102/app' })))
.toThrow(/path/);
});
test('requires a scenario outside public preflight and scenario-specific live intent', () => {
expect(() => readConfiguration(configEnv({ E2E_MODE: 'database-preflight' })))
.toThrow(/E2E_SCENARIO/);
const env = configEnv({
E2E_MODE: 'live-stagenet',
E2E_SCENARIO: 'direct-3x',
E2E_MAX_TRANSFER_ATOMIC: '500000000000',
E2E_CONFIRM: LIVE_CONFIRM,
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(/DIRECT_3X_TREASURE_ESCAPE/);
expect(() => assertLiveSafety(config, { ...env, E2E_CONFIRM: '' }))
.toThrow(/E2E_CONFIRM/);
const noNonceConfig = readConfiguration({ ...env, E2E_DATABASE_NONCE_FILE: '' });
expect(() => assertLiveSafety(noNonceConfig, env)).toThrow(/DATABASE_NONCE_FILE/);
expect(() => assertLiveSafety(
readConfiguration({ ...env, E2E_MAX_TRANSFER_ATOMIC: '' }), env
)).toThrow(/MAX_TRANSFER/);
});
test('builds exactly the current five-field stagenet acknowledgement', () => {
const disclosure = {
policyVersion: '2026-07-21-v2',
paidAcknowledgementRequired: true,
operatedProduct: {
id: OPERATED_PROFILE_ID,
scopeNotice: 'Single-player 2×/3× outcomes use test coins.',
noRealValueNotice: 'NO REAL VALUE'
},
service: {
cryptoType: 'XMR',
network: 'stagenet',
isTestNetwork: true,
paymentsEnabled: true,
directPaidEntryEnabled: true,
paidCreditsEnabled: false,
soloPayoutsEnabled: true,
anyPayoutsEnabled: true,
paidPrestigeOnly: false,
cryptoMatchPayoutsEnabled: false
}
};
const acknowledgement = canonicalAcknowledgement(disclosure);
expect(acknowledgement).toEqual({
policyVersion: '2026-07-21-v2',
ageEligible: true,
termsRead: true,
riskAccepted: true,
testnetUnderstood: true
});
expect(Object.keys(acknowledgement)).toHaveLength(5);
expect(Object.isFrozen(acknowledgement)).toBe(true);
expect(() => canonicalAcknowledgement({
...disclosure,
service: { ...disclosure.service, cryptoMatchPayoutsEnabled: true }
})).toThrow(/direct-only payout-enabled solo play/);
expect(() => canonicalAcknowledgement({
...disclosure,
operatedProduct: { ...disclosure.operatedProduct, id: 'unreviewed-profile' }
})).toThrow(/reviewed Such Software/);
expect(() => canonicalAcknowledgement({
policyVersion: 'v1',
paidAcknowledgementRequired: true,
operatedProduct: disclosure.operatedProduct,
service: {
cryptoType: 'XMR',
network: 'mainnet',
isTestNetwork: false,
paymentsEnabled: true,
directPaidEntryEnabled: true,
paidCreditsEnabled: false,
soloPayoutsEnabled: true,
anyPayoutsEnabled: true,
paidPrestigeOnly: false,
cryptoMatchPayoutsEnabled: false
}
})).toThrow(/stagenet/);
});
test('requires the exact public profile and 2x/3x solo outcomes with crypto PvP off', () => {
const modes = {
operatedProductProfileId: OPERATED_PROFILE_ID,
cryptoMatchPayoutsEnabled: false,
soloEnabled: true,
FREE: { enabled: true },
PAID_SINGLE: {
enabled: true,
payoutMultiplier: { escape: 2, escapeWithTreasure: 3 }
},
match: {
enabled: true,
economies: {
free: true,
credits_prestige: true
}
}
};
expect(() => assertPublicModeContract(modes)).not.toThrow();
expect(() => assertPublicModeContract({
...modes, operatedProductProfileId: 'unreviewed-profile'
})).toThrow(/reviewed no-crypto-PvP/);
expect(() => assertPublicModeContract({
...modes, cryptoMatchPayoutsEnabled: true
})).toThrow(/reviewed no-crypto-PvP/);
expect(() => assertPublicModeContract({
...modes,
soloEnabled: false
})).toThrow(/original solo mode/);
expect(() => assertPublicModeContract({
...modes,
match: { ...modes.match, economies: { free: true } }
})).toThrow(/free and credits-prestige/);
expect(() => assertPublicModeContract({
...modes,
match: {
...modes.match,
economies: {
...modes.match.economies,
crypto_race: true
}
}
})).toThrow(/free and credits-prestige/);
expect(() => assertPublicModeContract({
...modes,
match: {
...modes.match,
economies: {
...modes.match.economies,
future_paid_mode: true
}
}
})).toThrow(/free and credits-prestige/);
expect(() => assertPublicModeContract({
...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/);
expect(enabledEconomyIds({
free: true,
credits_prestige: true,
crypto_race: false
})).toEqual(['credits_prestige', 'free']);
});
test('requires a scenario-named dedicated canary E2E database', () => {
expect(() => validateExpectedDatabaseName(
'monerogue_canary_direct_e2e', SCENARIOS['direct-2x']
)).not.toThrow();
expect(() => validateExpectedDatabaseName(
'monerogue_canary_treasure_e2e', SCENARIOS['direct-3x']
)).not.toThrow();
expect(() => validateExpectedDatabaseName('monerogue', SCENARIOS['direct-2x']))
.toThrow(/canary and e2e/);
expect(() => validateExpectedDatabaseName(
'monerogue_canary_treasure_e2e', SCENARIOS['direct-2x']
)).toThrow(/scenario tag/);
});
test('freshness guard covers all financial/game ledgers and current migrations', async () => {
const row = {
users: 1,
seed_admins: 1,
required_migrations: REQUIRED_MIGRATIONS.length,
payments: 0,
games: 0,
payouts: 0,
receipts: 0,
refunds: 0,
late_reviews: 0,
credit_transactions: 0,
entitlement_grants: 0,
pack_entitlements: 0,
matches: 0,
match_entrants: 0,
match_events: 0,
match_queue_entries: 0,
race_entry_transactions: 0,
race_entry_lots: 0
};
const write = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
try {
const db = { query: jest.fn().mockResolvedValue({ rows: [row] }) };
await expect(assertEmptyFinancialDatabase(db)).resolves.toBeUndefined();
expect(db.query.mock.calls[0][0]).toContain('payment_receipts');
expect(db.query.mock.calls[0][0]).toContain('payment_entitlement_grants');
expect(db.query.mock.calls[0][0]).toContain('race_entry_lots');
const dirty = { query: jest.fn().mockResolvedValue({
rows: [{ ...row, receipts: 1 }]
}) };
await expect(assertEmptyFinancialDatabase(dirty)).rejects.toThrow(/receipts/);
} finally {
write.mockRestore();
}
});
test('verifies the app and harness exact database identity before the live runner', async () => {
const directory = fs.mkdtempSync('/tmp/wowngeon-canary-handshake-');
const nonceFile = path.join(directory, 'nonce');
const nonce = 'cd'.repeat(32);
fs.writeFileSync(nonceFile, `${nonce}\n`, { mode: 0o600 });
const config = readConfiguration(configEnv({
E2E_MODE: 'live-stagenet',
E2E_SCENARIO: 'direct-2x',
E2E_MAX_TRANSFER_ATOMIC: '1000',
E2E_DATABASE_NONCE_FILE: nonceFile
}));
const identity = {
clusterId: '7612345678901234567',
databaseOid: '24591',
databaseName: 'monerogue_canary_direct_e2e'
};
const db = { query: jest.fn().mockResolvedValue({ rows: [{
cluster_id: identity.clusterId,
database_oid: identity.databaseOid,
database_name: identity.databaseName
}] }) };
const request = jest.fn(async (_target, relative, options) => {
const challenge = options.headers['X-Canary-Database-Challenge'];
return {
response: {
status: 200,
headers: { get: name => name === 'cache-control' ? 'no-store' : null }
},
body: {
protocol: CANARY_HANDSHAKE_PROTOCOL,
challenge,
database: identity,
proof: computeProof(nonce, identity, challenge)
}
};
});
const write = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
try {
await expect(verifyCanaryDatabaseHandshake(config, db, {
E2E_EXPECT_DATABASE: identity.databaseName
}, request)).resolves.toBeUndefined();
expect(request).toHaveBeenCalledWith(config.target,
'/api/canary/database-identity', expect.any(Object));
} finally {
write.mockRestore();
fs.rmSync(directory, { recursive: true, force: true });
}
});
test('requires affirmative proof that invoice and funding wallets differ', () => {
expect(() => assertInvoiceNotOwned({
error: { message: "Address doesn't belong to the wallet" }
})).not.toThrow();
expect(() => assertInvoiceNotOwned({ result: { index: { major: 0, minor: 1 } } }))
.toThrow(/self-payment/);
expect(() => assertInvoiceNotOwned({ error: { message: 'daemon unavailable' } }))
.toThrow(/could not prove/);
});
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],
exit: [0, 1]
});
const directKnown = new Map();
addVisible(directKnown, directState);
expect(chooseMove(directKnown, directState, botState(), SCENARIOS['direct-2x']))
.toBe('down');
const treasureState = visibleState({
player: { x: 0, y: 0, hasTreasure: false },
treasure: [1, 0],
exit: [2, 0]
});
const treasureKnown = new Map();
addVisible(treasureKnown, treasureState);
const treasureBot = botState();
expect(chooseMove(treasureKnown, treasureState, treasureBot, SCENARIOS['direct-3x']))
.toBe('right');
const collected = {
...treasureState,
player: { x: 1, y: 0, hasTreasure: true },
treasure: null
};
expect(chooseMove(treasureKnown, collected, treasureBot, SCENARIOS['direct-3x']))
.toBe('right');
});
test.each([
['direct-2x', 10n, false],
['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);
const clientSeed = '22'.repeat(32);
const commitment = crypto.createHash('sha256').update(serverSeed).digest('hex');
const effectiveSeed = crypto.createHmac('sha256', serverSeed).update(clientSeed).digest('hex');
const offer = { offerId: '33'.repeat(32), commitment };
const gameId = 'game-proof-id';
const expected = base * BigInt(scenario.multiplier);
const gameOver = {
status: 'won',
reason: 'escaped',
treasure,
payout: {
success: true,
mode: scenario.gameMode,
payout: {
status: 'queued',
payoutId: 9,
amount: expected.toString(),
multiplier: scenario.multiplier
}
},
proof: {
gameId,
offerId: offer.offerId,
commitment,
clientSeed,
serverSeed,
effectiveSeed,
gameResult: { won: true, treasureFound: treasure }
}
};
const write = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
try {
expect(verifyGameOver(gameOver, {
scenario, payoutBase: base, offer, clientSeed
}, { proof: { gameId } })).toEqual({
expected,
payoutId: 9,
gameId
});
} finally {
write.mockRestore();
}
});
test('redacts addresses, hashes, tokens, credentials, and database URL passwords', () => {
const address = '5'.repeat(95);
const hash = 'a'.repeat(64);
const raw = `${address} ${hash} token=secret {"authorization":"jsonsecret"} Bearer othersecret `
+ 'postgres://alice:password@example.invalid/db';
const safe = safeMessage(raw);
expect(safe).toContain('[redacted-address]');
expect(safe).toContain('[redacted-64-byte-value]');
expect(safe).not.toContain(address);
expect(safe).not.toContain(hash);
expect(safe).not.toContain('secret');
expect(safe).not.toContain('password@example');
});
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).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('));
expect(mainBody.indexOf('assertLiveSafety(config, env)'))
.toBeLessThan(mainBody.indexOf('publicPreflight(config)'));
expect(mainBody.indexOf('verifyCanaryDatabaseHandshake(config, db, env)'))
.toBeLessThan(mainBody.indexOf('runLive(config, modules, db, legal, env)'));
const manifest = JSON.parse(fs.readFileSync(path.join(__dirname, '../src/package.json'), 'utf8'));
expect(manifest.scripts['canary:stagenet'])
.toBe('node scripts/stagenet-financial-canary.js');
});
test('runbook keeps the separate funding wallet off the house-wallet RPC port', () => {
const runbook = fs.readFileSync(RUNBOOK, 'utf8');
expect(runbook).toContain('port `38083` belongs to the promoted house wallet');
expect(runbook).not.toContain('E2E_FUNDING_RPC_URL=http://127.0.0.1:38083');
expect(runbook.match(/E2E_FUNDING_RPC_URL=http:\/\/127\.0\.0\.1:38085/g))
.toHaveLength(2);
});
});