Begin payment logic and tests

This commit is contained in:
jwinterm
2025-09-27 08:45:22 -04:00
parent 4ad063f735
commit e3915dcde2
29 changed files with 564835 additions and 853 deletions
+41
View File
@@ -0,0 +1,41 @@
const MovementManager = require('../src/game/movementManager');
function makeChaseGame() {
// Minimal game stub with player + monster and deterministic chase
return {
player: { x: 1, y: 1, getState() { return { x: this.x, y: this.y }; } },
monster: { x: 5, y: 1, getState() { return { x: this.x, y: this.y }; } },
movePlayer(dx, dy) {
this.player.x += dx; this.player.y += dy; return { status: 'moved' };
},
moveMonster() { // simple horizontal chase
if (this.monster.x > this.player.x) this.monster.x--; else if (this.monster.x < this.player.x) this.monster.x++;
if (this.monster.y > this.player.y) this.monster.y--; else if (this.monster.y < this.player.y) this.monster.y++;
},
getState() { return { player: { x: this.player.x, y: this.player.y }, monster: { x: this.monster.x, y: this.monster.y } }; }
};
}
describe('Monster chase integration (MovementManager hook)', () => {
test('monster advances toward player each move', () => {
const activeGames = new Map();
const socketId = 'sock-monster';
const game = makeChaseGame();
activeGames.set(socketId, game);
const emitted = [];
const io = { to: () => ({ emit: (evt, data) => emitted.push({ evt, data }) }) };
const debugManager = { getCurrentBlockHeight: () => 999 };
// Provide postMoveHook similar to server wiring
const mm = new MovementManager({ activeGames, io, debugManager, moveCooldown: 0, postMoveHook: ({ game }) => game.moveMonster() });
// Player moves right 3 times; monster should close distance
const startDistance = game.monster.x - game.player.x;
for (let i = 0; i < 3; i++) {
mm.handleMove(socketId, { direction: 'right' });
}
const endDistance = game.monster.x - game.player.x;
expect(endDistance).toBeLessThan(startDistance); // monster got closer
// Ensure we emitted 3 updates
expect(emitted.filter(e => e.evt === 'game_update').length).toBe(3);
});
});
+33
View File
@@ -0,0 +1,33 @@
const MovementManager = require('../src/game/movementManager');
// Minimal fake game implementing movePlayer & getState
function makeGame() {
return {
player: { x: 5, y: 5, getState() { return { x: this.x, y: this.y }; } },
movePlayer(dx, dy) {
this.player.x += dx;
this.player.y += dy;
return { status: 'moved' };
},
getState() { return { player: { x: this.player.x, y: this.player.y } }; }
};
}
describe('MovementManager', () => {
test('applies movement and rate limits subsequent rapid moves', () => {
const activeGames = new Map();
const socketId = 'sock1';
const game = makeGame();
activeGames.set(socketId, game);
const emitted = [];
const io = { to: () => ({ emit: (evt, data) => emitted.push({ evt, data }) }) };
const debugManager = { getCurrentBlockHeight: () => 123 };
const mm = new MovementManager({ activeGames, io, debugManager, moveCooldown: 100 });
mm.handleMove(socketId, { direction: 'right' });
mm.handleMove(socketId, { direction: 'right' }); // should be rate-limited
expect(game.player.x).toBe(6); // only first move applied
expect(emitted.filter(e => e.evt === 'game_update').length).toBe(1);
});
});
+42
View File
@@ -0,0 +1,42 @@
const PaymentHandlers = require('../src/network/paymentHandlers');
function makeMocks() {
return {
io: { to: () => ({ emit: () => {} }) },
gameModeManager: {
gameMode: 'PAID_SINGLE',
cryptoType: 'WOW',
singleGamePrice: 1000,
createPaymentRequest: async () => ({ id: 'pid123', address: 'addr', amount: 1000, expiresAt: new Date() })
},
walletService: {
startPaymentMonitoring: (addr, cb) => { /* simulate nothing */ },
stopPaymentMonitoring: () => {}
},
debugManager: { getCurrentBlockHeight: () => 123, CONSOLE_LOGGING: false },
queueManager: {
getUserBySocket: () => ({ clientId: 'client1' }),
getPlayerIndex: () => -1,
addPlayer: () => {},
markConfirmed: () => {}
},
broadcastManager: { sendStatusUpdate: () => {} }
};
}
describe('PaymentHandlers basic', () => {
test('createAndShowPaymentRequest emits payment_created', async () => {
const mocks = makeMocks();
const emitted = {};
mocks.io.to = () => ({ emit: (event, data) => { emitted[event] = data; } });
ph = new PaymentHandlers(mocks);
await ph.createAndShowPaymentRequest({ id: 'sock1' });
expect(emitted.payment_created).toBeDefined();
expect(emitted.payment_created.paymentId).toBe('pid123');
});
afterAll(() => {
if (ph && typeof ph.dispose === 'function') {
ph.dispose();
}
});
});
+117
View File
@@ -0,0 +1,117 @@
const QueueManager = require('../src/network/queueManager');
const PaymentHandlers = require('../src/network/paymentHandlers');
/**
* Integration-style test to ensure a payment that confirms AFTER a block event
* (where the entry was still unconfirmed) results in an immediate game start
* without waiting for the next block.
*/
describe('Payment flow integration: confirm-after-block immediate start', () => {
test('unconfirmed at block tick then confirmed triggers immediate game', async () => {
const events = [];
const activeGames = new Map();
const socketId = 'sock-int';
const blockHeight = 100;
// Mock IO emitter
const io = {
to: () => ({
emit: (event, data) => {
events.push({ event, data });
}
})
};
// Minimal broadcast manager stub
const broadcastManager = {
sendStatusUpdate: () => {},
broadcastChatMessage: () => {}
};
// Debug manager mock
const debugManager = {
CONSOLE_LOGGING: false,
getCurrentBlockHeight: () => blockHeight
};
// User lookup
const userObj = { id: socketId, clientId: 'client-int' };
const getUserBySocket = (id) => id === socketId ? userObj : null;
// createGameForUser mock (records active game & returns state)
const createGameForUser = (user) => {
const game = {
getState: () => ({ player: { x:0, y:0 }, lighting: {}, torches: [], started: true })
};
activeGames.set(user.id, game);
return game;
};
// Game mode manager mock (paid single game mode)
const gameModeManager = {
gameMode: 'PAID_SINGLE',
cryptoType: 'WOW',
singleGamePrice: 1000,
createPaymentRequest: async () => ({ id: 'pay-1', address: 'addr-1', amount: 1000, expiresAt: new Date(Date.now()+60000) })
};
// Wallet service mock with manual callback control
let monitorCallback;
const walletService = {
startPaymentMonitoring: (address, cb) => { monitorCallback = cb; },
stopPaymentMonitoring: () => {}
};
// Instantiate real QueueManager & PaymentHandlers
const queueManager = new QueueManager({
debugManager,
broadcastManager,
io,
createGameForUser,
getUserBySocket,
activeGames,
gameModeManager,
consoleLogging: false
});
const paymentHandlers = new PaymentHandlers({
io,
gameModeManager,
walletService,
debugManager,
queueManager,
broadcastManager
});
// Fake socket object with id
const fakeSocket = { id: socketId, emit: (event, data) => events.push({ event, data }) };
// Create payment request (does NOT queue yet) and start monitoring
await paymentHandlers.createAndShowPaymentRequest(fakeSocket);
expect(monitorCallback).toBeDefined();
// Simulate mempool detection first
monitorCallback({ in_mempool: true, confirmed: false, amount: 1000, confirmations: 0 });
// Should have queued the player but not started a game
expect(queueManager.getQueueLength()).toBe(1);
expect(events.filter(e => e.event === 'game_start').length).toBe(0);
// Simulate block tick BEFORE confirmation (should skip unconfirmed)
queueManager.startGamesForWaiting(blockHeight);
expect(events.filter(e => e.event === 'game_start').length).toBe(0);
// Entry should still be in queue (re-queued)
expect(queueManager.getQueueLength()).toBe(1);
// Now simulate confirmation after the block
monitorCallback({ in_mempool: true, confirmed: true, amount: 1000, confirmations: 1 });
// Immediate game start expected
const gameStarts = events.filter(e => e.event === 'game_start');
expect(gameStarts.length).toBe(1);
expect(gameStarts[0].data.blockHeight).toBe(blockHeight);
// Queue should now be empty
expect(queueManager.getQueueLength()).toBe(0);
});
});
+23
View File
@@ -0,0 +1,23 @@
const WalletRPCService = require('../src/payments/walletRPCService');
// Minimal debugManager mock
const debugManagerMock = { CONSOLE_LOGGING: false };
describe('WalletRPCService interface', () => {
test('exposes expected methods', () => {
const svc = new WalletRPCService(debugManagerMock);
expect(typeof svc.initialize).toBe('function');
expect(typeof svc.rpcCall).toBe('function');
expect(typeof svc.createPaymentRequest).toBe('function');
expect(typeof svc.checkPaymentStatus).toBe('function');
expect(typeof svc.startPaymentMonitoring).toBe('function');
expect(typeof svc.stopPaymentMonitoring).toBe('function');
expect(typeof svc.processBatchPayouts).toBe('function'); // newly added stub
});
test('processBatchPayouts stub returns processed:0', async () => {
const svc = new WalletRPCService(debugManagerMock);
const res = await svc.processBatchPayouts();
expect(res).toEqual({ processed: 0 });
});
});