Implemented and validated the separate Monero observation-worker path
This commit is contained in:
@@ -18,6 +18,7 @@ import { migrateCustody, grantCustodyRuntime, grantMoneroObserverRuntime, CUSTOD
|
||||
grantMoneroDepositCreditRuntime, CUSTODY_MIGRATION_V2, CUSTODY_SCHEMA_V2_DIGEST } from '../../server/market/postgres-schema.ts';
|
||||
import { persistMoneroObservationScan } from '../../server/market/monero-observation-store.ts';
|
||||
import { moneroWalletFingerprint } from '../../server/market/monero-observer.ts';
|
||||
import { runMoneroObserverBatch, runMoneroObserverWorker } from '../../server/market/monero-observer-worker.ts';
|
||||
import { depositCreditApprovalMessage, depositCreditReleaseApprovalMessage, depositReorgResolutionMessage,
|
||||
postReviewedDepositPendingCredit, prepareDepositCreditReleaseReview, prepareDepositCreditReview, prepareDepositReorgResolution,
|
||||
reconcileDepositCreditReorgHolds, releaseReviewedDepositCredit, resolveReleasedDepositReorgHold,
|
||||
@@ -413,6 +414,51 @@ test('Monero scan persistence is idempotent, reorg-visible and never posts custo
|
||||
} finally { await custodyRuntime.end(); await runtime.end(); }
|
||||
});
|
||||
|
||||
test('Monero observer worker catches up through bounded durable windows, resumes, and admits one leader', async t => {
|
||||
const f = await fixture(t);
|
||||
await f.admin.query("DO $$ BEGIN IF NOT EXISTS(SELECT 1 FROM pg_roles WHERE rolname='monero_worker_test') THEN CREATE ROLE monero_worker_test LOGIN; END IF; END $$");
|
||||
await grantMoneroObserverRuntime(f.admin, 'monero_worker_test');
|
||||
const workerPool = new Pool({ ...config, user: 'monero_worker_test' });
|
||||
const secondWorkerPool = new Pool({ ...config, user: 'monero_worker_test' });
|
||||
const wallet = '7'.repeat(64), walletHeight = 250_005, now = Date.now();
|
||||
const requested: number[] = [];
|
||||
const scanner = {
|
||||
async scanIncoming(fromHeight: number, maxBlocks = 100_000) {
|
||||
requested.push(fromHeight);
|
||||
const throughHeight = Math.min(walletHeight, fromHeight + maxBlocks - 1);
|
||||
return { network: 'mainnet' as const, walletHeight, fromHeight, throughHeight,
|
||||
primaryAddress: ADDITIONAL_MARKET_FEE.address, incoming: [] };
|
||||
},
|
||||
};
|
||||
try {
|
||||
const failedScanner = { scanIncoming: async () => { throw new Error('fixture RPC failure'); } };
|
||||
await assert.rejects(runMoneroObserverBatch(workerPool, wallet, failedScanner));
|
||||
assert.equal((await f.admin.query('SELECT count(*)::int AS n FROM wk_custody.monero_observer_state WHERE wallet_fingerprint=$1',
|
||||
[wallet])).rows[0].n, 0, 'failed RPC must not create or advance durable cursor state');
|
||||
|
||||
const batch = await runMoneroObserverBatch(workerPool, wallet, scanner, { now: () => now });
|
||||
assert.equal(batch.pages.length, 3, 'catch-up is split into a bounded number of pages');
|
||||
assert.equal(batch.caughtUp, true);
|
||||
assert.equal(batch.scanHeight, walletHeight);
|
||||
assert.deepEqual(requested, [0, 98_999, 197_998]);
|
||||
const restarted = await runMoneroObserverBatch(secondWorkerPool, wallet, scanner, { now: () => now + 1, maxPages: 1 });
|
||||
assert.equal(restarted.pages.length, 1);
|
||||
assert.equal(restarted.scanHeight, walletHeight);
|
||||
assert.equal(requested.at(-1), walletHeight - 1_000, 'restart resumes at a bounded reorg overlap');
|
||||
|
||||
const controller = new AbortController();
|
||||
let signalReady!: () => void;
|
||||
const batchObserved = new Promise<void>(resolve => { signalReady = resolve; });
|
||||
const firstWorker = runMoneroObserverWorker(workerPool, wallet, scanner, { pollIntervalMs: 60_000,
|
||||
signal: controller.signal, onBatch: () => signalReady() });
|
||||
await batchObserved;
|
||||
await assert.rejects(runMoneroObserverWorker(secondWorkerPool, wallet, scanner, { pollIntervalMs: 60_000 }), /already holds/);
|
||||
controller.abort(); await firstWorker;
|
||||
assert.equal((await f.admin.query('SELECT count(*)::int AS n FROM wk_custody.journals')).rows[0].n, 0,
|
||||
'observer worker only persists chain observations and never posts money');
|
||||
} finally { await secondWorkerPool.end(); await workerPool.end(); }
|
||||
});
|
||||
|
||||
test('reviewed Monero outputs post once to pending liability and reorgs create holds without reversal', async t => {
|
||||
const f = await fixture(t);
|
||||
await f.admin.query(`DO $$ BEGIN
|
||||
|
||||
Reference in New Issue
Block a user