Implemented and validated the separate Monero observation-worker path

This commit is contained in:
2026-10-22 09:38:20 -04:00
parent 12f80fdc53
commit 158981e965
12 changed files with 253 additions and 12 deletions
+10 -4
View File
@@ -2,25 +2,27 @@ import { Pool } from 'pg';
import { open, unlink, mkdir } from 'node:fs/promises';
import { resolve, join } from 'node:path';
import { postgresConfig } from '../server/market/postgres-config.ts';
import { migrateCustody, grantCustodyRuntime, grantMoneroDepositCreditRuntime } from '../server/market/postgres-schema.ts';
import { migrateCustody, grantCustodyRuntime, grantMoneroObserverRuntime,
grantMoneroDepositCreditRuntime } from '../server/market/postgres-schema.ts';
import { importSqliteCustody } from '../server/market/custody-import.ts';
import { PostgresCustody } from '../server/market/postgres-custody.ts';
import { IS_MAINNET } from '../shared/network.ts';
import { resolveCustodyIncident } from '../server/market/custody-incident.ts';
import { loadMoneroDepositReviewApproval, loadMoneroDepositReviewPolicy } from '../server/market/monero-deposit-review-config.ts';
import { parseCustodyDbArguments } from '../server/market/custody-db-args.ts';
import { assertCustodyDbActionAllowedOnNetwork, parseCustodyDbArguments } from '../server/market/custody-db-args.ts';
import { prepareDepositCreditReview, postReviewedDepositPendingCredit, prepareDepositCreditReleaseReview,
releaseReviewedDepositCredit, reconcileDepositCreditReorgHolds, prepareDepositReorgResolution,
resolveReleasedDepositReorgHold, type DepositCreditApproval, type DepositCreditReleaseApproval,
type DepositReorgResolutionApproval } from '../server/market/monero-deposit-credit.ts';
// Explicit operator task, never invoked by node startup. No passwords or database URLs printed.
if (IS_MAINNET) throw new Error('Only sandbox financial schema operations are implemented');
const commandArgs = process.argv.slice(2);
const depositActions = ['deposit-prepare-credit', 'deposit-stage-credit', 'deposit-prepare-release', 'deposit-release',
'deposit-reconcile-holds', 'deposit-prepare-resolve-hold', 'deposit-resolve-hold'];
const custodyActions = ['migrate', 'grant-runtime', 'grant-monero-credit-runtime', 'import-sqlite', 'audit', 'resolve-integrity'];
const custodyActions = ['migrate', 'grant-runtime', 'grant-monero-observer-runtime', 'grant-monero-credit-runtime',
'import-sqlite', 'audit', 'resolve-integrity'];
const { action, argument, extraArgument } = parseCustodyDbArguments(commandArgs);
assertCustodyDbActionAllowedOnNetwork(action, IS_MAINNET);
if (!custodyActions.includes(action) && !depositActions.includes(action)) throw new Error('Unsupported custody operation');
try {
const config = await postgresConfig();
@@ -32,6 +34,10 @@ try {
try {
if (action === 'migrate') { await migrateCustody(pool); console.log('Custody PostgreSQL schema verified/migrated. No funds activated.'); }
if (action === 'grant-runtime') { await grantCustodyRuntime(pool, argument ?? ''); console.log('Runtime grants applied; no credentials printed.'); }
if (action === 'grant-monero-observer-runtime') {
await grantMoneroObserverRuntime(pool, argument ?? '');
console.log('Separate Monero observation-only grants applied; journal access and deposit posting remain denied.');
}
if (action === 'grant-monero-credit-runtime') {
await grantMoneroDepositCreditRuntime(pool, argument ?? '');
console.log('Separate reviewed pending-credit coordinator grants applied; no credentials printed and no live deposits enabled.');
+54
View File
@@ -0,0 +1,54 @@
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;
}