import { z } from 'zod'; import type { PoolClient, QueryResult } from 'pg'; import { ADDITIONAL_MARKET_FEE } from '../../shared/market.ts'; import { parseAtomicXmr } from '../../shared/xmr-amount.ts'; import { allocateXmrSale } from '../../shared/xmr-pricing.ts'; import { validateXmrAddress } from './xmr.ts'; import { replayReservedSale } from './sale-rehearsal.ts'; /** Codes only: never attach customer IDs, payloads, SQL errors or addresses to operational diagnostics. */ export class CustodyIntegrityError extends Error { readonly code: string; constructor(code: string) { super(`Custody integrity check failed: ${code}`); this.code = code; } } function fail(code: string): never { throw new CustodyIntegrityError(code); } const uuid = z.string().uuid(), hex = z.string().regex(/^[a-f0-9]{64}$/); const integer = z.number().int().nonnegative().safe(); const atomic = z.string().max(22).refine(s => { try { parseAtomicXmr(s); return true; } catch { return false; } }); const positive = atomic.refine(s => { try { return parseAtomicXmr(s) > 0n; } catch { return false; } }); const wallet = z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{95}$/); const listingSchema = z.object({ id: uuid, seller: hex, cardHash: hex, sequence: integer, title: z.string().min(1).max(200), priceAtomicXmr: positive, feeBps: z.number().int().min(0).max(5000), hostFeeAddress: wallet.optional(), status: z.enum(['open', 'reserved', 'cancelled', 'sold']), createdAt: integer }).strict(); const orderSchema = z.object({ id: uuid, listingId: uuid, seller: hex, buyer: hex, cardHash: hex, sequence: integer, grossAtomicXmr: positive, sellerAtomicXmr: atomic, authorAtomicXmr: atomic, hostAtomicXmr: atomic, authorAddress: wallet, hostFeeAddress: wallet.optional(), status: z.enum(['awaiting-lock', 'cancelled', 'settled']), createdAt: integer }).strict(); const withdrawalSchema = z.object({ id: uuid, actor: hex, destination: z.literal('mock-external'), amountAtomicXmr: positive, maxFeeAtomicXmr: atomic, status: z.enum(['reserved', 'prepared', 'complete', 'cancelled']), attemptId: uuid.optional(), transactionId: z.string().regex(/^fixture:[a-f0-9]{64}$/).optional(), feeAtomicXmr: atomic.optional(), createdAt: integer }).strict(); type Entry = { account: string; debit: string; credit: string }; type Journal = { id: string; kind: string; lines: Entry[] }; function parse(schema: z.ZodType, data: unknown, code: string): T { const result = schema.safeParse(data); return result.success ? result.data : fail(code); } function same(a: unknown, b: unknown): boolean { // Compare parsed records without depending on JSONB object key ordering. if (a === b) return true; if (!a || !b || typeof a !== 'object' || typeof b !== 'object') return false; const x = a as Record, y = b as Record; const keys = Object.keys(x).filter(k => x[k] !== undefined).sort(); return keys.join('\0') === Object.keys(y).filter(k => y[k] !== undefined).sort().join('\0') && keys.every(k => same(x[k], y[k])); } function json(text: string | null): unknown { try { return JSON.parse(text ?? ''); } catch { return fail('outbox-encoding'); } } const d = (account: string, amount: string): Entry => ({ account, debit: amount, credit: '0' }); const c = (account: string, amount: string): Entry => ({ account, debit: '0', credit: amount }); async function journals(client: PoolClient, ids: string[]): Promise> { const result = await client.query(`SELECT j.id,j.kind,jsonb_agg(jsonb_build_object('account',e.account, 'debit',e.debit::text,'credit',e.credit::text) ORDER BY e.position) FILTER(WHERE e.journal IS NOT NULL) AS lines FROM wk_custody.journals j LEFT JOIN wk_custody.entries e ON e.journal=j.id WHERE j.id=ANY($1::text[]) GROUP BY j.id`, [ids]); return new Map(result.rows.map(r => [r.id, r as Journal])); } function expectJournal(all: Map, id: string, kind: string, expected: Entry[] | null) { const got = all.get(id); if (expected === null) { if (got) fail('unexpected-terminal-posting'); return; } if (!got || got.kind !== kind || !Array.isArray(got.lines)) fail('missing-lifecycle-posting'); const normalized = (lines: Entry[]) => lines.filter(l => BigInt(l.debit) !== 0n || BigInt(l.credit) !== 0n) .map(l => JSON.stringify([l.account, l.debit, l.credit])).sort(); if (!same(normalized(got.lines), normalized(expected))) fail('lifecycle-posting-mismatch'); } async function validateWallets(values: (string | undefined)[]) { // Bounded per-batch cache, not a growing process-wide map of customer data. for (const address of new Set(values.filter((v): v is string => !!v))) { try { await validateXmrAddress(address); } catch { fail('fee-recipient-invalid'); } } } type OrderRow = { id: string; buyer: string; seller: string; status: string; data: unknown; listing_data: unknown; listing_status: string; listing_card: string; balance: string | null; normal: string | null; box_kind: string | null; box_status: string | null; box_data: string | null }; async function checkOrders(client: PoolClient, rows: OrderRow[]) { const posting = await journals(client, rows.flatMap(r => [`reserve:${r.id}`, `cancel:${r.id}`, `settle:${r.id}`])); const wallets: string[] = []; for (const row of rows) { const o = parse(orderSchema, row.data, 'order-record-invalid'), l = parse(listingSchema, row.listing_data, 'order-listing-invalid'); if (o.id !== row.id || o.buyer !== row.buyer || o.seller !== row.seller || o.status !== row.status || l.status !== row.listing_status || l.cardHash !== row.listing_card || o.listingId !== l.id || o.seller !== l.seller || o.cardHash !== l.cardHash || o.sequence !== l.sequence || o.grossAtomicXmr !== l.priceAtomicXmr || o.seller === o.buyer || (o.hostFeeAddress ?? ADDITIONAL_MARKET_FEE.address) !== (l.hostFeeAddress ?? ADDITIONAL_MARKET_FEE.address)) fail('order-terms-mismatch'); const split = allocateXmrSale(BigInt(l.priceAtomicXmr), l.feeBps); if (o.sellerAtomicXmr !== split.sellerBeforeNetworkCosts.toString() || o.hostAtomicXmr !== split.hosting.toString() || o.authorAtomicXmr !== split.author.toString() || o.authorAddress !== ADDITIONAL_MARKET_FEE.address) fail('order-allocation-mismatch'); wallets.push(o.authorAddress, o.hostFeeAddress ?? ADDITIONAL_MARKET_FEE.address); const account = `customer:${o.buyer}:purchase:${o.id}`, available = `customer:${o.buyer}:available`; if (row.normal !== 'credit' || row.balance !== (o.status === 'awaiting-lock' ? o.grossAtomicXmr : '0')) fail('order-reservation-mismatch'); const expectedStatus = o.status === 'awaiting-lock' ? 'reserved' : o.status === 'settled' ? 'sold' : 'cancelled'; if (l.status !== expectedStatus || row.box_kind !== 'nft-lock') fail('order-state-mismatch'); if (o.status === 'settled') { if (row.box_status !== 'fixture-complete' || !/^fixture:[a-f0-9]{64}$/.test(row.box_data ?? '')) fail('settlement-evidence-mismatch'); } else { const original = parse(orderSchema, json(row.box_data), 'order-outbox-invalid'); if (row.box_status !== (o.status === 'cancelled' ? 'cancelled' : 'blocked') || !same(original, { ...o, status: 'awaiting-lock' })) fail('order-outbox-mismatch'); } expectJournal(posting, `reserve:${o.id}`, 'purchase-reservation', [d(available, o.grossAtomicXmr), c(account, o.grossAtomicXmr)]); expectJournal(posting, `cancel:${o.id}`, 'purchase-cancellation', o.status === 'cancelled' ? [d(account, o.grossAtomicXmr), c(available, o.grossAtomicXmr)] : null); expectJournal(posting, `settle:${o.id}`, 'mock-sale-settlement', o.status === 'settled' ? [d(account, o.grossAtomicXmr), c(`customer:${o.seller}:available`, o.sellerAtomicXmr), c(`author:${o.authorAddress}`, o.authorAtomicXmr), c(o.hostFeeAddress ? `revenue:host:${o.hostFeeAddress}` : 'revenue:host', o.hostAtomicXmr)] : null); const evidenceRows = await client.query('SELECT key,value FROM wk_custody.custody_meta WHERE key=ANY($1::text[])', [[`sale-rehearsal-binding:${o.id}`, `sale-rehearsal-observation:${o.id}`, `sale-rehearsal-terminal:${o.id}`, `sale-rehearsal-terminal-index:${o.id}`]]); if (evidenceRows.rowCount) { try { const records = new Map(evidenceRows.rows.map(r => [r.key, r.value])); const binding = records.get(`sale-rehearsal-binding:${o.id}`); const identity = await client.query("SELECT value FROM wk_custody.custody_meta WHERE key='identity'"); const verify = (key: string) => { const evidence = JSON.parse(records.get(key) ?? ''); const replay = replayReservedSale({ ...o, status: 'awaiting-lock' }, JSON.parse(identity.rows[0].value).id, evidence.input, l.feeBps); if (evidence.version !== 1 || evidence.binding !== binding || replay.binding !== binding || replay.stateRoot !== evidence.stateRoot) fail('sale-rehearsal-evidence-mismatch'); return replay; }; verify(`sale-rehearsal-observation:${o.id}`); if (o.status === 'awaiting-lock') { if (records.has(`sale-rehearsal-terminal:${o.id}`) || records.has(`sale-rehearsal-terminal-index:${o.id}`)) { fail('sale-rehearsal-evidence-mismatch'); } } else { const replay = verify(`sale-rehearsal-terminal:${o.id}`); if (replay.decision.action !== (o.status === 'settled' ? 'settle' : 'refund') || (o.status === 'settled' && row.box_data !== `fixture:${replay.stateRoot}`)) fail('sale-rehearsal-evidence-mismatch'); const index = JSON.parse(records.get(`sale-rehearsal-terminal-index:${o.id}`) ?? ''); if (index.version !== 1 || index.stateRoot !== replay.stateRoot || index.terminalTransactionId !== (replay.terminalTransactionId ?? null)) fail('sale-rehearsal-evidence-mismatch'); if (replay.terminalTransactionId) { const link = await client.query('SELECT value FROM wk_custody.custody_meta WHERE key=$1', [`sale-rehearsal-tx:${replay.terminalTransactionId}`]); const parsedLink = JSON.parse(link.rows[0]?.value ?? ''); if (parsedLink.version !== 1 || parsedLink.orderId !== o.id || parsedLink.stateRoot !== replay.stateRoot) { fail('sale-rehearsal-terminal-index-mismatch'); } } } } catch { fail('sale-rehearsal-evidence-mismatch'); } } } await validateWallets(wallets); } const orderSelect = `SELECT o.id::text,o.buyer,o.seller,o.status,o.data,l.data AS listing_data,l.status AS listing_status, l.card AS listing_card,a.balance::text,a.normal,b.kind AS box_kind,b.status AS box_status,b.data AS box_data FROM wk_custody.orders o LEFT JOIN wk_custody.listings l ON l.id::text=o.data->>'listingId' LEFT JOIN wk_custody.accounts a ON a.id='customer:'||o.buyer||':purchase:'||o.id::text LEFT JOIN wk_custody.outbox b ON b.id=o.id`; type WithdrawalRow = { id: string; actor: string; status: string; data: unknown; balance: string | null; normal: string | null; box_kind: string | null; box_status: string | null; box_data: string | null }; async function checkWithdrawals(client: PoolClient, rows: WithdrawalRow[]) { const posting = await journals(client, rows.flatMap(r => [`withdraw:${r.id}`, `withdraw-cancel:${r.id}`, `withdraw-confirm:${r.id}`])); for (const row of rows) { const w = parse(withdrawalSchema, row.data, 'withdrawal-record-invalid'); if (w.id !== row.id || w.actor !== row.actor || w.status !== row.status) fail('withdrawal-identity-mismatch'); const total = (BigInt(w.amountAtomicXmr) + BigInt(w.maxFeeAtomicXmr)).toString(); const active = w.status === 'reserved' || w.status === 'prepared'; if (row.normal !== 'credit' || row.balance !== (active ? total : '0')) fail('withdrawal-reservation-mismatch'); const prepared = w.status === 'prepared' || w.status === 'complete'; if (prepared ? !w.attemptId || !w.transactionId || w.feeAtomicXmr === undefined || BigInt(w.feeAtomicXmr) > BigInt(w.maxFeeAtomicXmr) : w.attemptId !== undefined || w.transactionId !== undefined || w.feeAtomicXmr !== undefined) fail('withdrawal-attempt-invalid'); const status = w.status === 'reserved' ? 'blocked' : w.status === 'cancelled' ? 'cancelled' : w.status === 'prepared' ? 'fixture-prepared' : 'fixture-complete'; const box = parse(withdrawalSchema, json(row.box_data), 'withdrawal-outbox-invalid'); if (row.box_kind !== 'withdrawal' || row.box_status !== status || !same(box, { ...w, status: prepared ? 'prepared' : 'reserved' })) fail('withdrawal-outbox-mismatch'); const account = `customer:${w.actor}:withdrawal:${w.id}`, available = `customer:${w.actor}:available`; expectJournal(posting, `withdraw:${w.id}`, 'withdrawal-reservation', [d(available, total), c(account, total)]); expectJournal(posting, `withdraw-cancel:${w.id}`, 'withdrawal-cancellation', w.status === 'cancelled' ? [d(account, total), c(available, total)] : null); expectJournal(posting, `withdraw-confirm:${w.id}`, 'mock-withdrawal', w.status === 'complete' ? [d(account, total), c('treasury:mock', (BigInt(w.amountAtomicXmr) + BigInt(w.feeAtomicXmr!)).toString()), c(available, (BigInt(w.maxFeeAtomicXmr) - BigInt(w.feeAtomicXmr!)).toString())] : null); } } const withdrawalSelect = `SELECT w.id::text,w.actor,w.status,w.data,a.balance::text,a.normal, b.kind AS box_kind,b.status AS box_status,b.data AS box_data FROM wk_custody.withdrawals w LEFT JOIN wk_custody.accounts a ON a.id='customer:'||w.actor||':withdrawal:'||w.id::text LEFT JOIN wk_custody.outbox b ON b.id=w.id`; /** Targeted mutation checks share the same transaction as postings/receipts. No external side effects. */ export async function auditOrder(client: PoolClient, id: string) { const result = await client.query(`${orderSelect} WHERE o.id=$1`, [id]); if (result.rowCount !== 1) fail('order-record-missing'); await checkOrders(client, result.rows); } export async function auditWithdrawal(client: PoolClient, id: string) { const result = await client.query(`${withdrawalSelect} WHERE w.id=$1`, [id]); if (result.rowCount !== 1) fail('withdrawal-record-missing'); await checkWithdrawals(client, result.rows); } /** Caller supplies a coherent transaction. Keyset pages bound application memory, not total audit duration. */ export async function auditBusinessLiabilities(client: PoolClient): Promise { const deadline = performance.now() + 15000; const budget = () => { if (performance.now() > deadline) throw new Error('Custody business audit budget exceeded'); }; let after: string | null = null; for (;;) { budget(); const result: QueryResult = await client.query(`${orderSelect} WHERE ($1::uuid IS NULL OR o.id>$1::uuid) ORDER BY o.id LIMIT 128`, [after]); await checkOrders(client, result.rows); if (result.rows.length < 128) break; after = result.rows.at(-1)!.id; } after = null; for (;;) { budget(); const result: QueryResult = await client.query(`${withdrawalSelect} WHERE ($1::uuid IS NULL OR w.id>$1::uuid) ORDER BY w.id LIMIT 128`, [after]); await checkWithdrawals(client, result.rows); if (result.rows.length < 128) break; after = result.rows.at(-1)!.id; } after = null; for (;;) { budget(); const result: QueryResult<{ id: string; card: string; status: string; data: unknown }> = await client.query('SELECT id::text,card,status,data FROM wk_custody.listings WHERE ($1::uuid IS NULL OR id>$1::uuid) ORDER BY id LIMIT 128', [after]); const wallets: string[] = []; for (const row of result.rows) { const l = parse(listingSchema, row.data, 'listing-record-invalid'); if (row.id !== l.id || row.card !== l.cardHash || row.status !== l.status) fail('listing-identity-mismatch'); wallets.push(l.hostFeeAddress ?? ADDITIONAL_MARKET_FEE.address); } await validateWallets(wallets); if (result.rows.length < 128) break; after = result.rows.at(-1)!.id; } // Check both directions: a correct order cannot excuse orphan reserves, tasks or terminal postings. const checks: [string, string][] = [ ['orphan-purchase-reserve', `SELECT 1 FROM wk_custody.accounts a WHERE a.id LIKE 'customer:%:purchase:%' AND NOT EXISTS(SELECT 1 FROM wk_custody.orders o WHERE a.id='customer:'||o.buyer||':purchase:'||o.id::text) LIMIT 1`], ['orphan-withdrawal-reserve', `SELECT 1 FROM wk_custody.accounts a WHERE a.id LIKE 'customer:%:withdrawal:%' AND NOT EXISTS(SELECT 1 FROM wk_custody.withdrawals w WHERE a.id='customer:'||w.actor||':withdrawal:'||w.id::text) LIMIT 1`], ['orphan-outbox', `SELECT 1 FROM wk_custody.outbox b WHERE NOT EXISTS(SELECT 1 FROM wk_custody.orders o WHERE o.id=b.id AND b.kind='nft-lock') AND NOT EXISTS(SELECT 1 FROM wk_custody.withdrawals w WHERE w.id=b.id AND b.kind='withdrawal') LIMIT 1`], ['listing-order-mismatch', `SELECT 1 FROM wk_custody.listings l LEFT JOIN wk_custody.orders o ON o.data->>'listingId'=l.id::text WHERE (l.status IN ('reserved','sold') AND o.id IS NULL) OR l.id::text IS DISTINCT FROM l.data->>'id' OR l.status IS DISTINCT FROM l.data->>'status' OR l.card IS DISTINCT FROM l.data->>'cardHash' LIMIT 1`], ['duplicate-listing-orders', `SELECT 1 FROM wk_custody.orders GROUP BY data->>'listingId' HAVING count(*)<>1 LIMIT 1`], ['orphan-lifecycle-posting', `SELECT 1 FROM wk_custody.journals j WHERE (j.kind IN ('purchase-reservation','purchase-cancellation','mock-sale-settlement') AND NOT EXISTS (SELECT 1 FROM wk_custody.orders o WHERE j.id IN ('reserve:'||o.id::text,'cancel:'||o.id::text,'settle:'||o.id::text))) OR (j.kind IN ('withdrawal-reservation','withdrawal-cancellation','mock-withdrawal') AND NOT EXISTS (SELECT 1 FROM wk_custody.withdrawals w WHERE j.id IN ('withdraw:'||w.id::text,'withdraw-cancel:'||w.id::text,'withdraw-confirm:'||w.id::text))) LIMIT 1`], ['duplicate-prepared-attempt', `SELECT 1 FROM wk_custody.withdrawals WHERE status IN ('prepared','complete') GROUP BY data->>'attemptId' HAVING count(*)>1 LIMIT 1`], ['duplicate-prepared-transaction', `SELECT 1 FROM wk_custody.withdrawals WHERE status IN ('prepared','complete') GROUP BY data->>'transactionId' HAVING count(*)>1 LIMIT 1`], ]; for (const [code, sql] of checks) { budget(); if ((await client.query(sql)).rowCount) fail(code); } }