171 lines
12 KiB
TypeScript
Executable File
171 lines
12 KiB
TypeScript
Executable File
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, grantMoneroObserverRuntime,
|
|
grantMoneroDepositCreditRuntime, grantMoneroDepositAllocatorRuntime, grantMoneroWithdrawalRuntime,
|
|
grantPilotDepositReservationRuntime, grantPilotDepositAddressRuntime } 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';
|
|
import { IS_MAINNET, IS_PILOT } from '../shared/network.ts';
|
|
import { initializePilotCustodyIdentity, parsePilotCustodianId } from '../server/market/pilot-custody-identity.ts';
|
|
import { resolveCustodyIncident } from '../server/market/custody-incident.ts';
|
|
import { loadMoneroDepositReviewApproval, loadMoneroDepositReviewPolicy } from '../server/market/monero-deposit-review-config.ts';
|
|
import { assertCustodyDbActionAllowedOnNetwork, assertMainnetSchemaMigrationAcknowledged,
|
|
assertMainnetCustodyOperationAcknowledged, assertPilotCustodyActionAllowed, assertPilotSchemaMigrationAcknowledged,
|
|
assertPilotCustodyIdentityAcknowledged, assertPilotReservationGrantAcknowledged,
|
|
assertPilotAddressGrantAcknowledged,
|
|
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.
|
|
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', 'migrate-mainnet-schema', 'initialize-mainnet-custody-identity', 'migrate-pilot-schema',
|
|
'initialize-pilot-custody-identity', 'grant-runtime', 'grant-monero-observer-runtime',
|
|
'grant-pilot-deposit-reservation-runtime',
|
|
'grant-pilot-deposit-address-runtime',
|
|
'grant-monero-withdrawal-runtime',
|
|
'grant-monero-deposit-allocator-runtime', 'grant-monero-credit-runtime',
|
|
'import-sqlite', 'audit', 'resolve-integrity'];
|
|
const { action, argument, extraArgument } = parseCustodyDbArguments(commandArgs);
|
|
assertCustodyDbActionAllowedOnNetwork(action, IS_MAINNET);
|
|
assertPilotCustodyActionAllowed(action, IS_PILOT);
|
|
assertMainnetSchemaMigrationAcknowledged(action, process.env.WIKIDECK_MAINNET_SCHEMA_MIGRATION);
|
|
assertMainnetCustodyOperationAcknowledged(action, process.env.WIKIDECK_MAINNET_CUSTODY_OPERATIONS);
|
|
assertPilotSchemaMigrationAcknowledged(action, process.env.WIKIDECK_PILOT_SCHEMA_MIGRATION);
|
|
assertPilotCustodyIdentityAcknowledged(action, process.env.WIKIDECK_PILOT_CUSTODY_IDENTITY,
|
|
process.env.WIKIDECK_PILOT_CUSTODY_OPERATIONS);
|
|
assertPilotReservationGrantAcknowledged(action, process.env.WIKIDECK_PILOT_CUSTODY_OPERATIONS);
|
|
assertPilotAddressGrantAcknowledged(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');
|
|
}
|
|
if (!custodyActions.includes(action) && !depositActions.includes(action)) throw new Error('Unsupported custody operation');
|
|
try {
|
|
const config = await postgresConfig();
|
|
if (action === 'audit') {
|
|
const market = await PostgresCustody.open(config, { feeBps: 250, card: () => undefined });
|
|
try { console.log(JSON.stringify(await market.audit())); } finally { await market.close(); }
|
|
} else {
|
|
const pool = new Pool(config);
|
|
try {
|
|
if (action === 'migrate' || action === 'migrate-mainnet-schema' || action === 'migrate-pilot-schema') {
|
|
await migrateCustody(pool);
|
|
console.log(action === 'migrate-mainnet-schema'
|
|
? 'Mainnet custody schema verified/migrated; no runtime grants, deposits, or payments enabled.'
|
|
: action === 'migrate-pilot-schema'
|
|
? 'Pilot custody schema verified/migrated; no pilot identity, runtime grants, deposits, or payments enabled.'
|
|
: 'Custody PostgreSQL schema verified/migrated. No funds activated.');
|
|
}
|
|
if (action === 'initialize-mainnet-custody-identity') {
|
|
if (!IS_MAINNET || extraArgument) throw new Error('Expected mainnet initialize-mainnet-custody-identity CUSTODIAN_UUID');
|
|
const identity = await initializeMainnetCustodyIdentity(pool, parseMainnetCustodianId(argument ?? ''));
|
|
console.log(JSON.stringify({ initialized: true, network: identity.network, chainId: identity.chainId,
|
|
custodianId: identity.id, crediting: 'reviewed-pending-only', withdrawals: 'disabled' }));
|
|
}
|
|
if (action === 'initialize-pilot-custody-identity') {
|
|
if (!IS_PILOT || process.env.WIKIDECK_PILOT_MODE !== 'private-mock-only' || extraArgument) {
|
|
throw new Error('Expected pilot profile, private-mock-only mode, and one custodian UUID');
|
|
}
|
|
const identity = await initializePilotCustodyIdentity(pool, parsePilotCustodianId(argument ?? ''));
|
|
console.log(JSON.stringify({ initialized: true, marketNetwork: identity.marketNetwork,
|
|
marketChainId: identity.marketChainId, moneroNetwork: identity.moneroNetwork,
|
|
custodianId: identity.custodianId, activation: identity.activationStatus,
|
|
deposits: 'disabled', withdrawals: 'disabled' }));
|
|
}
|
|
if (action === 'grant-pilot-deposit-reservation-runtime') {
|
|
if (!IS_PILOT || process.env.WIKIDECK_PILOT_CUSTODY_OPERATIONS !== 'reservation-only-no-addresses' || extraArgument) {
|
|
throw new Error('Pilot reservation grant requires the pilot profile, reservation-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 the separate pilot custody identity before granting reservation access');
|
|
await grantPilotDepositReservationRuntime(pool, argument ?? '');
|
|
console.log('Pilot durable index-reservation role granted; no Wallet-RPC address creation, credit, or withdrawal access.');
|
|
}
|
|
if (action === 'grant-pilot-deposit-address-runtime') {
|
|
if (!IS_PILOT || process.env.WIKIDECK_PILOT_CUSTODY_OPERATIONS !== 'operator-triggered-addresses-only' || extraArgument) {
|
|
throw new Error('Pilot address grant requires pilot profile, operator-triggered 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 address allocation access');
|
|
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-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-deposit-allocator-runtime') {
|
|
await grantMoneroDepositAllocatorRuntime(pool, argument ?? '');
|
|
console.log('Separate Monero deposit-allocation grants applied; balance, journal, observation, and credit tables remain denied.');
|
|
}
|
|
if (action === 'grant-monero-withdrawal-runtime') {
|
|
await grantMoneroWithdrawalRuntime(pool, argument ?? '');
|
|
console.log('Separate customer-signed withdrawal-reservation grants applied; transaction signing and relay are not granted.');
|
|
}
|
|
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.');
|
|
}
|
|
if (action === 'resolve-integrity') console.log(JSON.stringify(await resolveCustodyIncident(pool, argument ?? '')));
|
|
if (depositActions.includes(action)) {
|
|
const policy = await loadMoneroDepositReviewPolicy();
|
|
if (action === 'deposit-prepare-credit') {
|
|
if (!argument || !extraArgument) throw new Error('Expected deposit-prepare-credit WALLET_FINGERPRINT OBSERVATION_ID');
|
|
console.log(JSON.stringify(await prepareDepositCreditReview(pool, argument, extraArgument, policy)));
|
|
}
|
|
if (action === 'deposit-stage-credit') {
|
|
if (!argument || extraArgument) throw new Error('Expected deposit-stage-credit APPROVAL_JSON_FILE');
|
|
const approval = await loadMoneroDepositReviewApproval(argument);
|
|
// The coordinator validates the exact approval schema/signature before any write.
|
|
console.log(JSON.stringify(await postReviewedDepositPendingCredit(pool, approval as DepositCreditApproval, policy)));
|
|
}
|
|
if (action === 'deposit-prepare-release') {
|
|
if (!argument || extraArgument) throw new Error('Expected deposit-prepare-release CREDIT_UUID');
|
|
console.log(JSON.stringify(await prepareDepositCreditReleaseReview(pool, argument, policy)));
|
|
}
|
|
if (action === 'deposit-release') {
|
|
if (!argument || extraArgument) throw new Error('Expected deposit-release APPROVAL_JSON_FILE');
|
|
const approval = await loadMoneroDepositReviewApproval(argument);
|
|
// The coordinator validates the exact approval schema/signature before any write.
|
|
console.log(JSON.stringify(await releaseReviewedDepositCredit(pool, approval as DepositCreditReleaseApproval, policy)));
|
|
}
|
|
if (action === 'deposit-reconcile-holds') {
|
|
if (!argument || extraArgument) throw new Error('Expected deposit-reconcile-holds WALLET_FINGERPRINT');
|
|
console.log(JSON.stringify({ holdsCreated: await reconcileDepositCreditReorgHolds(pool, argument) }));
|
|
}
|
|
if (action === 'deposit-prepare-resolve-hold') {
|
|
if (!argument || !extraArgument) throw new Error('Expected deposit-prepare-resolve-hold CREDIT_UUID HOLD_UUID');
|
|
console.log(JSON.stringify(await prepareDepositReorgResolution(pool, argument, extraArgument, policy)));
|
|
}
|
|
if (action === 'deposit-resolve-hold') {
|
|
if (!argument || extraArgument) throw new Error('Expected deposit-resolve-hold APPROVAL_JSON_FILE');
|
|
const approval = await loadMoneroDepositReviewApproval(argument);
|
|
// The coordinator validates the exact approval schema/signature before any write.
|
|
console.log(JSON.stringify(await resolveReleasedDepositReorgHold(pool, approval as DepositReorgResolutionApproval, policy)));
|
|
}
|
|
}
|
|
if (action === 'import-sqlite') {
|
|
if (!argument) throw new Error('Stopped source node DATA_DIR required');
|
|
const directory = resolve(argument);
|
|
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
const lockPath = join(directory, 'testnet-node.lock'), lock = await open(lockPath, 'wx', 0o600);
|
|
try {
|
|
await lock.writeFile(`custody-import:${process.pid}`);
|
|
console.log(JSON.stringify(await importSqliteCustody(join(directory, 'testnet', 'custody-sandbox.sqlite'), pool)));
|
|
} finally { await lock.close(); await unlink(lockPath); }
|
|
}
|
|
} finally { await pool.end(); }
|
|
}
|
|
} catch {
|
|
console.error('Custody database operation failed. Check configuration, schema, stopped-node lock and migration state privately. No automatic fallback/reset was performed.');
|
|
process.exitCode = 1;
|
|
}
|