Implemented and validated the separate Monero observation-worker path
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { parseCustodyDbArguments } from '../server/market/custody-db-args.ts';
|
||||
import { assertCustodyDbActionAllowedOnNetwork, parseCustodyDbArguments } from '../server/market/custody-db-args.ts';
|
||||
|
||||
test('custody database CLI parses two-argument deposit operations without shifting arguments', () => {
|
||||
assert.deepEqual(parseCustodyDbArguments(['deposit-prepare-credit', 'a'.repeat(64), `${'b'.repeat(64)}:${'c'.repeat(64)}`]), {
|
||||
@@ -9,6 +9,17 @@ test('custody database CLI parses two-argument deposit operations without shifti
|
||||
assert.deepEqual(parseCustodyDbArguments(['deposit-prepare-resolve-hold', 'credit-id', 'hold-id']), {
|
||||
action: 'deposit-prepare-resolve-hold', argument: 'credit-id', extraArgument: 'hold-id',
|
||||
});
|
||||
assert.deepEqual(parseCustodyDbArguments(['grant-monero-observer-runtime', 'monero_observer']), {
|
||||
action: 'grant-monero-observer-runtime', argument: 'monero_observer', extraArgument: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
test('mainnet custody CLI permits only the observation-role grant', () => {
|
||||
assert.doesNotThrow(() => assertCustodyDbActionAllowedOnNetwork('grant-monero-observer-runtime', true));
|
||||
for (const action of ['migrate', 'grant-runtime', 'grant-monero-credit-runtime', 'audit', 'deposit-release'] as const) {
|
||||
assert.throws(() => assertCustodyDbActionAllowedOnNetwork(action, true), /only the separately scoped/i);
|
||||
assert.doesNotThrow(() => assertCustodyDbActionAllowedOnNetwork(action, false));
|
||||
}
|
||||
});
|
||||
|
||||
test('custody database CLI rejects unknown, missing, empty, and excess arguments', () => {
|
||||
|
||||
@@ -234,7 +234,7 @@ test('observer refuses non-loopback plaintext, URL credentials and malformed hei
|
||||
assert.throws(() => createMoneroWalletObserver({ ...config, url: 'http://wallet.example/json_rpc' }, transport), /HTTPS/);
|
||||
assert.throws(() => createMoneroWalletObserver({ ...config, url: 'http://localhost/json_rpc' }, transport), /HTTPS/);
|
||||
assert.throws(() => createMoneroWalletObserver({ ...config, url: 'https://user:pass@wallet.example/json_rpc' }, transport), /bare/);
|
||||
await assert.rejects(observer.scanIncoming(-1), /height/);
|
||||
await assert.rejects(observer.scanIncoming(-1), /scan range/);
|
||||
await assert.rejects(observer.scanIncoming(101), /ahead/);
|
||||
});
|
||||
|
||||
|
||||
@@ -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