115 lines
6.4 KiB
TypeScript
Executable File
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();
|
|
}
|