Files
wikideck/server/market/pilot-custody-identity.ts

115 lines
6.4 KiB
TypeScript
Executable File

import type { Pool, PoolClient } from 'pg';
import { IS_PILOT, NETWORK } from '../../shared/network.ts';
import { CHAIN_ID } from '../testnet/consensus.ts';
import { CUSTODY_WRITE_LOCK, verifyCustodySchema } from './postgres-schema.ts';
export type PilotCustodyIdentity = {
version: 1;
mode: 'pilot-custody';
policy: 'pilot-custody-v1';
marketNetwork: 'wikideck-randomx-pilot-1';
marketChainId: string;
moneroNetwork: 'mainnet';
custodianId: string;
activationStatus: 'identity-only';
initializedAt: number;
};
function validUuid(value: string): boolean {
return /^[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(value);
}
function requirePilot(custodianId: string, initializedAt: number): void {
if (!IS_PILOT || NETWORK !== 'wikideck-randomx-pilot-1' || !validUuid(custodianId)) {
throw new Error('Pilot custody identity requires the pilot profile and a valid custodian UUID');
}
if (!Number.isSafeInteger(initializedAt) || initializedAt < 0) throw new Error('Invalid pilot identity initialization time');
}
export function pilotCustodyIdentityFromRow(row: Record<string, unknown>): PilotCustodyIdentity {
const identity = {
version: Number(row.version), mode: row.mode, policy: row.policy,
marketNetwork: row.market_network, marketChainId: row.market_chain_id,
moneroNetwork: row.monero_network, custodianId: row.custodian_id,
activationStatus: row.activation_status, initializedAt: Number(row.initialized_at),
} as PilotCustodyIdentity;
if (identity.version !== 1 || identity.mode !== 'pilot-custody' || identity.policy !== 'pilot-custody-v1' ||
identity.marketNetwork !== 'wikideck-randomx-pilot-1' || identity.marketChainId !== CHAIN_ID ||
identity.moneroNetwork !== 'mainnet' || !validUuid(identity.custodianId) ||
identity.activationStatus !== 'identity-only' || !Number.isSafeInteger(identity.initializedAt) || identity.initializedAt < 0) {
throw new Error('Stored pilot custody identity is invalid or inconsistent');
}
return identity;
}
async function requireEmptyFinancialDatabase(client: PoolClient, allowPilotIdentity = false): Promise<void> {
const result = await client.query(`SELECT NOT wk_custody.pilot_reservation_database_isolated() AS populated,
(SELECT count(*) FROM wk_custody.pilot_custody_identity) AS pilot_identities`);
if (result.rows[0].populated) {
throw new Error('Pilot identity requires a separate empty custody database; existing mock or financial data cannot be reused');
}
const identities = Number(result.rows[0].pilot_identities);
if ((!allowPilotIdentity && identities !== 0) || (allowPilotIdentity && identities !== 1)) {
throw new Error('Pilot custody database identity state is inconsistent');
}
if (!allowPilotIdentity) {
const reservations = await client.query('SELECT EXISTS(SELECT 1 FROM wk_custody.pilot_monero_deposit_reservations) AS populated');
if (reservations.rows[0].populated) throw new Error('Pilot identity requires an empty pilot reservation table');
}
}
/** Recheck the dedicated database boundary before each reservation; only its single identity row is allowed. */
export async function assertPilotReservationDatabaseEmpty(client: PoolClient): Promise<void> {
await requireEmptyFinancialDatabase(client, true);
}
/** One-time identity binding only. It does not enable deposit allocation, observation, credit, or withdrawal roles. */
export async function initializePilotCustodyIdentity(pool: Pool, custodianId: string,
initializedAt = Date.now()): Promise<PilotCustodyIdentity> {
requirePilot(custodianId, initializedAt);
const expected: PilotCustodyIdentity = { version: 1, mode: 'pilot-custody', policy: 'pilot-custody-v1',
marketNetwork: 'wikideck-randomx-pilot-1', marketChainId: CHAIN_ID, moneroNetwork: 'mainnet',
custodianId: custodianId.toLowerCase(), activationStatus: 'identity-only', initializedAt };
const client = await pool.connect();
try {
await client.query('BEGIN');
await client.query("SET LOCAL statement_timeout='10s'; SET LOCAL lock_timeout='3s'; SET LOCAL synchronous_commit=on");
await client.query('SELECT pg_advisory_xact_lock($1::bigint)', [CUSTODY_WRITE_LOCK]);
await verifyCustodySchema(client);
const current = await client.query('SELECT * FROM wk_custody.pilot_custody_identity WHERE singleton=1 FOR UPDATE');
if (current.rowCount) {
const identity = pilotCustodyIdentityFromRow(current.rows[0]);
if (identity.version !== expected.version || identity.mode !== expected.mode || identity.policy !== expected.policy ||
identity.marketNetwork !== expected.marketNetwork || identity.marketChainId !== expected.marketChainId ||
identity.moneroNetwork !== expected.moneroNetwork || identity.custodianId !== expected.custodianId) {
throw new Error('Pilot custody database is already bound to a different market, Monero network, or custodian');
}
await client.query('COMMIT');
return identity;
}
await requireEmptyFinancialDatabase(client);
await client.query(`INSERT INTO wk_custody.pilot_custody_identity
(singleton,version,mode,policy,market_network,market_chain_id,monero_network,custodian_id,activation_status,initialized_at)
VALUES(1,$1,$2,$3,$4,$5,$6,$7,$8,$9)`, [expected.version, expected.mode, expected.policy,
expected.marketNetwork, expected.marketChainId, expected.moneroNetwork, expected.custodianId,
expected.activationStatus, expected.initializedAt]);
await client.query('COMMIT');
return expected;
} catch (error) { await client.query('ROLLBACK').catch(() => {}); throw error; }
finally { client.release(); }
}
export async function verifyPilotCustodyIdentity(pool: Pool, custodianId: string): Promise<PilotCustodyIdentity> {
requirePilot(custodianId, 0);
const result = await pool.query('SELECT * FROM wk_custody.pilot_custody_identity WHERE singleton=1');
if (result.rowCount !== 1) throw new Error('Pilot custody identity is not initialized');
const identity = pilotCustodyIdentityFromRow(result.rows[0]);
if (identity.custodianId !== custodianId.toLowerCase()) throw new Error('Pilot custody identity does not match configured custodian');
return identity;
}
export function parsePilotCustodianId(value: string): string {
if (!validUuid(value)) throw new Error('Invalid pilot custodian UUID');
return value.toLowerCase();
}