Added pilot-bound Monero observation persistence and the one-shot monero:pilot-observe command
This commit is contained in:
+14
-1
@@ -4,7 +4,8 @@ import { resolve, join } from 'node:path';
|
||||
import { postgresConfig } from '../server/market/postgres-config.ts';
|
||||
import { migrateCustody, grantCustodyRuntime, grantMoneroObserverRuntime,
|
||||
grantMoneroDepositCreditRuntime, grantMoneroDepositAllocatorRuntime, grantMoneroWithdrawalRuntime,
|
||||
grantPilotDepositReservationRuntime, grantPilotDepositAddressRuntime } from '../server/market/postgres-schema.ts';
|
||||
grantPilotDepositReservationRuntime, grantPilotDepositAddressRuntime,
|
||||
grantPilotMoneroObserverRuntime } from '../server/market/postgres-schema.ts';
|
||||
import { initializeMainnetCustodyIdentity, parseMainnetCustodianId } from '../server/market/mainnet-custody-identity.ts';
|
||||
import { importSqliteCustody } from '../server/market/custody-import.ts';
|
||||
import { PostgresCustody } from '../server/market/postgres-custody.ts';
|
||||
@@ -16,6 +17,7 @@ import { assertCustodyDbActionAllowedOnNetwork, assertMainnetSchemaMigrationAckn
|
||||
assertMainnetCustodyOperationAcknowledged, assertPilotCustodyActionAllowed, assertPilotSchemaMigrationAcknowledged,
|
||||
assertPilotCustodyIdentityAcknowledged, assertPilotReservationGrantAcknowledged,
|
||||
assertPilotAddressGrantAcknowledged,
|
||||
assertPilotObserverGrantAcknowledged,
|
||||
parseCustodyDbArguments } from '../server/market/custody-db-args.ts';
|
||||
import { prepareDepositCreditReview, postReviewedDepositPendingCredit, prepareDepositCreditReleaseReview,
|
||||
releaseReviewedDepositCredit, reconcileDepositCreditReorgHolds, prepareDepositReorgResolution,
|
||||
@@ -30,6 +32,7 @@ const custodyActions = ['migrate', 'migrate-mainnet-schema', 'initialize-mainnet
|
||||
'initialize-pilot-custody-identity', 'grant-runtime', 'grant-monero-observer-runtime',
|
||||
'grant-pilot-deposit-reservation-runtime',
|
||||
'grant-pilot-deposit-address-runtime',
|
||||
'grant-pilot-monero-observer-runtime',
|
||||
'grant-monero-withdrawal-runtime',
|
||||
'grant-monero-deposit-allocator-runtime', 'grant-monero-credit-runtime',
|
||||
'import-sqlite', 'audit', 'resolve-integrity'];
|
||||
@@ -43,6 +46,7 @@ assertPilotCustodyIdentityAcknowledged(action, process.env.WIKIDECK_PILOT_CUSTOD
|
||||
process.env.WIKIDECK_PILOT_CUSTODY_OPERATIONS);
|
||||
assertPilotReservationGrantAcknowledged(action, process.env.WIKIDECK_PILOT_CUSTODY_OPERATIONS);
|
||||
assertPilotAddressGrantAcknowledged(action, process.env.WIKIDECK_PILOT_CUSTODY_OPERATIONS);
|
||||
assertPilotObserverGrantAcknowledged(action, process.env.WIKIDECK_PILOT_CUSTODY_OPERATIONS);
|
||||
if (action === 'initialize-mainnet-custody-identity' && process.env.WIKIDECK_MAINNET_CUSTODY_IDENTITY !== 'initialize-mainnet-custody-v1') {
|
||||
throw new Error('Mainnet custody identity initialization requires WIKIDECK_MAINNET_CUSTODY_IDENTITY=initialize-mainnet-custody-v1');
|
||||
}
|
||||
@@ -97,6 +101,15 @@ try {
|
||||
await grantPilotDepositAddressRuntime(pool, argument ?? '');
|
||||
console.log('Pilot Wallet-RPC address allocator role granted for explicit operator action only; no HTTP enrollment or financial posting.');
|
||||
}
|
||||
if (action === 'grant-pilot-monero-observer-runtime') {
|
||||
if (!IS_PILOT || process.env.WIKIDECK_PILOT_CUSTODY_OPERATIONS !== 'pilot-observation-only-no-credit' || extraArgument) {
|
||||
throw new Error('Pilot observer grant requires pilot profile, observation-only acknowledgement, and one role name');
|
||||
}
|
||||
const identity = await pool.query('SELECT 1 FROM wk_custody.pilot_custody_identity WHERE singleton=1');
|
||||
if (!identity.rowCount) throw new Error('Initialize pilot custody identity before granting observation access');
|
||||
await grantPilotMoneroObserverRuntime(pool, argument ?? '');
|
||||
console.log('Pilot Monero observation-only role granted; no address allocation, credit, withdrawal, or public enrollment privileges.');
|
||||
}
|
||||
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 ?? '');
|
||||
|
||||
Executable
+68
@@ -0,0 +1,68 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { Pool } from 'pg';
|
||||
import { IS_PILOT } from '../shared/network.ts';
|
||||
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 { parsePilotCustodianId, verifyPilotCustodyIdentity } from '../server/market/pilot-custody-identity.ts';
|
||||
import { readPilotMoneroObservationCursor, persistPilotMoneroObservationScan } from '../server/market/pilot-monero-observation-store.ts';
|
||||
|
||||
if (process.platform === 'win32') throw new Error('Pilot observer requires POSIX deployment with protected-file permission checks');
|
||||
if (!IS_PILOT || process.env.WIKIDECK_PILOT_MODE !== 'private-mock-only') {
|
||||
throw new Error('Pilot observer requires the private mock-only pilot profile');
|
||||
}
|
||||
if (process.env.WIKIDECK_PILOT_CUSTODY_OPERATIONS !== 'pilot-observation-only-no-credit') {
|
||||
throw new Error('Set WIKIDECK_PILOT_CUSTODY_OPERATIONS=pilot-observation-only-no-credit for this explicit observer action');
|
||||
}
|
||||
|
||||
const custodianId = parsePilotCustodianId(process.env.WIKIDECK_PILOT_CUSTODIAN_ID ?? '');
|
||||
const walletGenerationId = process.env.WIKIDECK_MONERO_WALLET_GENERATION_ID ?? '';
|
||||
const expectedFingerprint = process.env.WIKIDECK_MONERO_WALLET_FINGERPRINT ?? '';
|
||||
if (!/^[a-f0-9-]{36}$/i.test(walletGenerationId) || !/^[a-f0-9]{64}$/.test(expectedFingerprint)) {
|
||||
throw new Error('Invalid protected pilot watch-only wallet generation or fingerprint');
|
||||
}
|
||||
function positiveBounded(name: string, fallback: number, maximum: number): number {
|
||||
const value = process.env[name];
|
||||
if (value === undefined) return fallback;
|
||||
if (!/^[1-9]\d{0,5}$/.test(value)) throw new Error(`Invalid ${name}`);
|
||||
const parsed = Number(value);
|
||||
if (!Number.isSafeInteger(parsed) || parsed > maximum) throw new Error(`Invalid ${name}`);
|
||||
return parsed;
|
||||
}
|
||||
const maxBlocks = positiveBounded('WIKIDECK_PILOT_OBSERVER_MAX_BLOCKS', 5000, 100_000);
|
||||
const overlap = positiveBounded('WIKIDECK_PILOT_OBSERVER_OVERLAP', 100, 10_000);
|
||||
|
||||
const observerConfig = await loadMoneroObserverConfig({ ...process.env, WIKIDECK_MONERO_OBSERVER: '1' });
|
||||
const transportConfig = await loadMoneroMtlsTransportConfig();
|
||||
if (observerConfig.network !== 'mainnet' || new URL(observerConfig.url).href !== new URL(transportConfig.url).href) {
|
||||
throw new Error('Pilot observer requires matching Mainnet Wallet-RPC and mTLS gateway endpoints');
|
||||
}
|
||||
const scanner = createMoneroWalletObserver(observerConfig, await loadMoneroMtlsTransport(transportConfig));
|
||||
const primaryAddress = await scanner.verifyMainnetWallet();
|
||||
const walletFingerprint = await moneroWalletFingerprint(primaryAddress);
|
||||
if (walletFingerprint !== expectedFingerprint) throw new Error('Configured pilot wallet fingerprint does not match protected Wallet-RPC');
|
||||
|
||||
const dbUrlFile = process.env.WIKIDECK_PILOT_OBSERVER_PG_URL_FILE;
|
||||
const dbCaFile = process.env.WIKIDECK_PILOT_OBSERVER_PG_CA_FILE;
|
||||
if (!dbUrlFile || !dbCaFile) throw new Error('Pilot observer PostgreSQL secret URL and CA files are required');
|
||||
const pool = new Pool(await postgresConfig({ ...process.env,
|
||||
WIKIDECK_CUSTODY_PG_URL_FILE: dbUrlFile,
|
||||
WIKIDECK_CUSTODY_PG_CA_FILE: dbCaFile,
|
||||
WIKIDECK_CUSTODY_PG_TRANSPORT: 'tls' }));
|
||||
pool.on('error', () => console.error(JSON.stringify({ component: 'pilot-monero-observer', event: 'database-unavailable' })));
|
||||
try {
|
||||
const identity = await verifyPilotCustodyIdentity(pool, custodianId);
|
||||
const scanHeight = await readPilotMoneroObservationCursor(pool, walletGenerationId, walletFingerprint);
|
||||
const fromHeight = Math.max(0, scanHeight - overlap);
|
||||
const page = await scanner.scanIncoming(fromHeight, maxBlocks);
|
||||
const result = await persistPilotMoneroObservationScan(pool, { scanId: randomUUID(), walletGenerationId,
|
||||
walletFingerprint, expectedScanHeight: scanHeight, scannedAt: Date.now(), page });
|
||||
console.log(JSON.stringify({ component: 'pilot-monero-observer', network: identity.moneroNetwork,
|
||||
marketNetwork: identity.marketNetwork, walletHeight: page.walletHeight, scanHeight: result.scanHeight,
|
||||
caughtUp: result.scanHeight >= page.walletHeight, observed: result.observed, assigned: result.assigned,
|
||||
unallocated: result.unallocated, changed: result.changed, reappeared: result.reappeared,
|
||||
notSeen: result.notSeen, replayed: result.replayed, credits: 'disabled' }));
|
||||
} catch {
|
||||
console.error('Pilot observation failed or requires review; no balance credit was performed.');
|
||||
process.exitCode = 1;
|
||||
} finally { await pool.end(); }
|
||||
Reference in New Issue
Block a user