Files
wownerogue/test/paymentProviderWiring_ultra.test.js
T
jwinterm 74753c17b2 feat(payments): wire provider registry into invoice creation + fit real BTCPay infra
Recon of the operator's LAN BTCPay box (ssh four-btcpay / 10.42.1.33) showed the
real topology: BTCPay Server 2.3.9 handles BTC/LTC only (BTCPAY_CHAINS=ltc,btc),
while XMR and WOW run through separate 'xmrcheckout' (:8080) and 'wowcheckout'
(:8180) apps — and crucially, both expose a BTCPay-Greenfield-COMPATIBLE API
(/api/v1/stores/{id}/invoices, token auth, Settled/New/Expired status). Grin
isn't deployed there yet. So one Greenfield client class serves all three.

- btcpayProvider: id is now constructor-configurable so the same class registers
  as 'btcpay' (BTC/LTC), 'xmrcheckout' (XMR), and 'wowcheckout' (WOW).
- buildProviderRegistry: activates each gateway independently from its *_URL/
  _STORE_ID/_API_KEY trio (BTCPAY_*, XMRCHECKOUT_*, WOWCHECKOUT_*), with
  topology-correct default routing (BTC/LTC->btcpay, XMR/WOW->their checkout or
  native), BTCPAY_CHAINS to widen, PAYMENT_ROUTING to override.
- gameModeManager: takes an optional paymentProviders registry (defaults to a
  native-wrapping registry built from walletService). createPaymentRequest now
  creates the invoice via getProvider(cryptoType).createInvoice(...). The native
  provider delegates to walletService.createPaymentRequest, so the shipped
  single-chain config is byte-for-byte unchanged — same subaddress, same
  monitoring maps. paymentRequest now also carries invoiceId for the monitoring
  wire to come.
- nativeMoneroProvider: surfaces addressIndex so the call site stays provider-agnostic.

Behavior-preserving: no gateway env => native-only, every chain routes native.
Monitoring + payout paths intentionally left on the native path this pass;
gateway monitoring (provider.startWatch) is the flagged next step. Tests: +4
(provider wiring routes to a gateway; default registry delegates to native).
Suite 53 suites / 365 tests green.
2026-07-10 17:17:49 -04:00

69 lines
3.3 KiB
JavaScript

/**
* Proves GameModeManager.createPaymentRequest routes invoice creation through the payment-provider
* registry: a configured gateway is used, and with the default registry it delegates to the native
* walletService (byte-for-byte legacy path).
*/
const GameModeManager = require('../src/game/gameModeManager');
function mockDb() {
return {
query: jest.fn(async (sql) => {
const s = String(sql);
if (/FROM users WHERE id/i.test(s)) return { rows: [{ id: 7, credits: 0, total_credits_purchased: 0 }] };
if (/INSERT INTO payments/i.test(s)) return { rows: [{ id: 101, expires_at: null }] };
return { rows: [] }; // reuse lookup, expire-stale, everything else
}),
withTransaction: jest.fn(async (cb) => cb({ query: jest.fn().mockResolvedValue({ rows: [] }) }))
};
}
const mockDebug = { CONSOLE_LOGGING: false };
const mockConfig = {
getConfig: () => ({
paymentsEnabled: true,
currency: { symbol: 'WOW', decimals: 11 },
modes: { direct: { enabled: true, price: 100000000000n }, credits: { enabled: false, packages: [] } },
payouts: { rules: { direct: { multipliers: {} }, credits: { enabled: false, multipliers: {} } } },
preferences: {}
}),
getLegacyGameMode: () => 'PAID_SINGLE',
eventBus: { on: () => {} }
};
describe('createPaymentRequest routes through the provider registry', () => {
test('a configured gateway provider is used for invoice creation', async () => {
const gateway = {
id: 'xmrcheckout',
createInvoice: jest.fn(async () => ({ invoiceId: 'inv-uuid', address: 'GATEWAYADDR', addressIndex: null, expiresAt: null })),
getProvider() { return this; }
};
const registry = { getProvider: jest.fn(() => gateway) };
const walletService = { createPaymentRequest: jest.fn() };
const gmm = new GameModeManager(mockDb(), walletService, mockDebug, mockConfig, registry);
const res = await gmm.createPaymentRequest('sock1', 'single_game', { userId: 7 });
expect(registry.getProvider).toHaveBeenCalledWith(gmm.cryptoType);
expect(gateway.createInvoice).toHaveBeenCalledWith(
expect.objectContaining({ chain: gmm.cryptoType, userId: 7, orderId: 'sock1' })
);
expect(res.address).toBe('GATEWAYADDR');
expect(res.invoiceId).toBe('inv-uuid');
expect(walletService.createPaymentRequest).not.toHaveBeenCalled(); // routed away from native
});
test('the default registry delegates to the native walletService (legacy path preserved)', async () => {
const walletService = {
createPaymentRequest: jest.fn(async () => ({ address: 'NATIVEADDR', addressIndex: 3, expiresAt: null }))
};
// No injected registry -> constructor builds one whose only provider wraps this walletService.
const gmm = new GameModeManager(mockDb(), walletService, mockDebug, mockConfig);
const res = await gmm.createPaymentRequest('sock2', 'single_game', { userId: 7 });
expect(walletService.createPaymentRequest).toHaveBeenCalledWith(expect.anything(), expect.any(String), 7, 'sock2');
expect(res.address).toBe('NATIVEADDR');
expect(res.invoiceId).toBe('NATIVEADDR'); // native invoices keyed by subaddress
});
});