import { Pool } from 'pg'; import { createMoneroWalletObserver, loadMoneroObserverConfig, moneroWalletFingerprint } from '../server/market/monero-observer.ts'; import { loadMoneroMtlsTransport, loadMoneroMtlsTransportConfig } from '../server/market/monero-mtls-transport.ts'; import { postgresConfig } from '../server/market/postgres-config.ts'; import { runMoneroObserverWorker } from '../server/market/monero-observer-worker.ts'; if (process.platform === 'win32') throw new Error('Monero observer worker requires a POSIX deployment with verifiable protected-file permissions'); if (process.env.WIKIDECK_MONERO_OBSERVER_WORKER !== '1') throw new Error('Set WIKIDECK_MONERO_OBSERVER_WORKER=1 to start the private observer worker'); const observerConfig = await loadMoneroObserverConfig(); const transportConfig = await loadMoneroMtlsTransportConfig(); if (new URL(observerConfig.url).href !== new URL(transportConfig.url).href) { throw new Error('Observer and mTLS transport endpoints do not match'); } const transport = await loadMoneroMtlsTransport(transportConfig); const scanner = createMoneroWalletObserver(observerConfig, transport); const primaryAddress = await scanner.verifyMainnetWallet(); const walletFingerprint = await moneroWalletFingerprint(primaryAddress); const pool = new Pool(await postgresConfig()); pool.on('error', () => console.error(JSON.stringify({ component: 'monero-observer', event: 'database-unavailable' }))); const abort = new AbortController(); const stop = () => abort.abort(); process.once('SIGINT', stop); process.once('SIGTERM', stop); try { await runMoneroObserverWorker(pool, walletFingerprint, scanner, { pollIntervalMs: parsePollInterval(process.env.WIKIDECK_MONERO_OBSERVER_POLL_MS), signal: abort.signal, onBatch(result) { console.log(JSON.stringify({ component: 'monero-observer', event: 'scan-complete', scanHeight: result.scanHeight, walletHeight: result.walletHeight, caughtUp: result.caughtUp, pages: result.pages.length, observed: result.pages.reduce((sum, page) => sum + page.observed, 0), changed: result.pages.reduce((sum, page) => sum + page.changed, 0), reappeared: result.pages.reduce((sum, page) => sum + page.reappeared, 0), notSeen: result.pages.reduce((sum, page) => sum + page.notSeen, 0) })); }, onFailure() { console.error(JSON.stringify({ component: 'monero-observer', event: 'scan-failed', action: 'retrying' })); }, }); } finally { process.off('SIGINT', stop); process.off('SIGTERM', stop); await pool.end(); } function parsePollInterval(value: string | undefined): number { if (value === undefined) return 30_000; if (!/^[1-9]\d{0,5}$/.test(value)) throw new Error('Invalid Monero observer polling interval'); const interval = Number(value); if (!Number.isSafeInteger(interval) || interval < 1_000 || interval > 15 * 60_000) { throw new Error('Invalid Monero observer polling interval'); } return interval; }