Files
wownerogue/test/walletNetworkIdentity.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

146 lines
6.1 KiB
JavaScript

const WalletRPCService = require('../src/payments/walletRPCService');
const ADDRESSES = Object.freeze({
'XMR:mainnet': `4${'1'.repeat(94)}`,
'XMR:stagenet': `5${'1'.repeat(94)}`,
'XMR:testnet': `9${'1'.repeat(94)}`,
'WOW:mainnet': `Wo${'1'.repeat(95)}`
});
function walletHttp({ address, network, height = 123, valid = true }) {
return {
post: jest.fn(async (_url, body) => {
if (body.method === 'get_version') return { data: { result: { version: 1 } } };
if (body.method === 'get_height') return { data: { result: { height } } };
if (body.method === 'get_address') return { data: { result: { address } } };
if (body.method === 'validate_address') {
return { data: { result: { valid, nettype: network } } };
}
if (body.method === 'create_address') {
return { data: { result: { address, address_index: 1 } } };
}
throw new Error(`unexpected method: ${body.method}`);
})
};
}
describe('WalletRPCService production identity', () => {
const debugManager = { CONSOLE_LOGGING: false };
test.each([
['XMR', 'mainnet'],
['XMR', 'stagenet'],
['XMR', 'testnet'],
['WOW', 'mainnet']
])('accepts a validated %s %s primary address', async (cryptoType, network) => {
const address = ADDRESSES[`${cryptoType}:${network}`];
const service = new WalletRPCService(debugManager, {
http: walletHttp({ address, network }),
walletEndpoint: 'http://wallet.test',
cryptoType,
network,
identityRequired: true
});
await expect(service.initialize()).resolves.toBe(true);
expect(service.getIdentityStatus()).toMatchObject({
required: true,
verified: true,
expected: { cryptoType, network },
actual: { cryptoType, network }
});
expect(JSON.stringify(service.getHealthStatus())).not.toContain(address);
});
test.each([
['wrong chain', 'WOW', 'mainnet', ADDRESSES['XMR:mainnet'], 'mainnet'],
['wrong network', 'XMR', 'mainnet', ADDRESSES['XMR:stagenet'], 'stagenet']
])('fails initialization for the %s without logging the address', async (
_label, cryptoType, expectedNetwork, address, actualNetwork
) => {
const error = jest.spyOn(console, 'error').mockImplementation(() => {});
const service = new WalletRPCService(debugManager, {
http: walletHttp({ address, network: actualNetwork }),
walletEndpoint: 'http://wallet.test',
cryptoType,
network: expectedNetwork,
identityRequired: true
});
await expect(service.initialize()).resolves.toBe(false);
expect(service.getIdentityStatus().verified).toBe(false);
expect(error.mock.calls.flat().join(' ')).not.toContain(address);
error.mockRestore();
});
test('blocks money-changing calls when the configured identity does not match', async () => {
const http = walletHttp({
address: ADDRESSES['XMR:mainnet'],
network: 'mainnet'
});
const service = new WalletRPCService(debugManager, {
http,
walletEndpoint: 'http://wallet.test',
cryptoType: 'WOW',
network: 'mainnet',
identityRequired: true
});
const error = jest.spyOn(console, 'error').mockImplementation(() => {});
await expect(service.createPaymentRequest(1, 'entry', 'user', 'socket'))
.rejects.toMatchObject({ code: 'WALLET_IDENTITY_MISMATCH' });
expect(http.post.mock.calls.map(([, body]) => body.method)).not.toContain('create_address');
expect(error.mock.calls.flat().join(' ')).not.toContain(ADDRESSES['XMR:mainnet']);
error.mockRestore();
});
test('development may connect for diagnostics but remains explicitly unverified', async () => {
const service = new WalletRPCService(debugManager, {
http: walletHttp({ address: ADDRESSES['XMR:stagenet'], network: 'stagenet' }),
walletEndpoint: 'http://wallet.test',
cryptoType: 'XMR',
network: 'mainnet',
identityRequired: false
});
await expect(service.initialize()).resolves.toBe(true);
expect(service.getIdentityStatus()).toMatchObject({ required: false, verified: false });
});
test('a transport-only liveness response cannot revive stale production identity', async () => {
const service = new WalletRPCService(debugManager, {
http: walletHttp({ address: ADDRESSES['XMR:stagenet'], network: 'stagenet' }),
walletEndpoint: 'http://wallet.test',
cryptoType: 'XMR',
network: 'stagenet',
identityRequired: true,
identityMaxAgeMs: 1000
});
await expect(service.initialize()).resolves.toBe(true);
service.networkIdentity.checkedAt = Date.now() - 2000;
service.isHealthy = false;
await expect(service.rpcCall('get_version')).resolves.toBeDefined();
expect(service.isHealthy).toBe(false);
expect(service.getHealthStatus().healthy).toBe(false);
expect(service.getIdentityStatus().verified).toBe(false);
});
test('rejects a zero-height wallet and redacts URL credentials from health', async () => {
const error = jest.spyOn(console, 'error').mockImplementation(() => {});
const service = new WalletRPCService(debugManager, {
http: walletHttp({ address: ADDRESSES['XMR:stagenet'], network: 'stagenet', height: 0 }),
walletEndpoint: 'http://rpc-user:rpc-password@wallet.test',
cryptoType: 'XMR',
network: 'stagenet',
identityRequired: true
});
await expect(service.initialize()).resolves.toBe(false);
expect(service.getHealthStatus().endpoint).toBe('http://wallet.test');
expect(JSON.stringify(service.getHealthStatus())).not.toMatch(/rpc-user|rpc-password/);
error.mockRestore();
});
});