Files
wikideck/server/market/postgres-custody.ts
T

420 lines
31 KiB
TypeScript
Executable File

import { Pool, type PoolClient, type PoolConfig } from 'pg';
import { createHash, randomUUID } from 'node:crypto';
import { IS_MAINNET, NETWORK } from '../../shared/network.ts';
import { CHAIN_ID, verifySignature } from '../testnet/consensus.ts';
import { Ledger } from '../testnet/ledger.ts';
import { ADDITIONAL_MARKET_FEE } from '../../shared/market.ts';
import { allocateXmrSale } from '../../shared/xmr-pricing.ts';
import { parseAtomicXmr } from '../../shared/xmr-amount.ts';
import { CUSTODY_POLICY, MOCK_CREDIT_ATOMIC, custodyCommandSchema, custodyMessage,
type CustodyActivity, type CustodyCommand, type CustodyInfo, type CustodyListing,
type CustodyOrder, type CustodyPage, type CustodyWithdrawal } from '../../shared/custody.ts';
import { CustodyError } from './custody.ts';
import { CUSTODY_WRITE_LOCK, verifyCustodySchema } from './postgres-schema.ts';
import { resolvedMarketPolicy, validateMarketPolicy, permitsMarketAction, type MarketPolicy } from './policy.ts';
import { CustodyIntegrityError, auditBusinessLiabilities, auditOrder, auditWithdrawal } from './custody-integrity.ts';
import { replayReservedSale } from './sale-rehearsal.ts';
import { CanonicalInboxError, auditCanonicalInbox, ingestCanonicalPage } from './canonical-inbox.ts';
export type CustodyOptions = { feeBps: number; card(hash: string, owner: string, sequence: number): { title: string } | undefined;
now?: () => number; disposableSaleRehearsal?: boolean } & MarketPolicy;
type Result = CustodyActivity | CustodyListing | CustodyOrder | CustodyWithdrawal;
export type CustodyAudit = { journals: number; assetsAtomicXmr: string; claimsAndRevenueAtomicXmr: string };
type Line = { account: string; normal?: 'debit' | 'credit'; debit?: bigint; credit?: bigint };
const available = (actor: string) => `customer:${actor}:available`;
const reserved = (o: CustodyOrder) => `customer:${o.buyer}:purchase:${o.id}`;
const held = (w: CustodyWithdrawal) => `customer:${w.actor}:withdrawal:${w.id}`;
function check(ok: unknown, message: string): asserts ok { if (!ok) throw new CustodyError(message); }
/** PostgreSQL-backed SAME sandbox protocol. No real XMR, NFT witness, or automatic schema migration. */
export class PostgresCustody {
readonly id: string;
private readonly pool: Pool;
private readonly options: CustodyOptions;
private active = 0;
private readonly auditTimer: ReturnType<typeof setInterval>;
private auditInFlight: Promise<CustodyAudit> | undefined;
private integrityHold = false;
private auditState: { status: 'ok' | 'failed'; lastAttemptAt: number; lastSuccessAt: number; failureCode: string | null };
private constructor(pool: Pool, options: CustodyOptions, id: string) {
this.pool = pool; this.options = options; this.id = id;
this.auditState = { status: 'ok', lastAttemptAt: Date.now(), lastSuccessAt: Date.now(), failureCode: null };
this.auditTimer = setInterval(() => { void this.audit().catch(() => {
console.error('Custody financial audit unavailable or inconsistent; mutations held pending operator review.');
}); }, 60000);
this.auditTimer.unref();
}
static async open(config: PoolConfig, options: CustodyOptions): Promise<PostgresCustody> {
if (IS_MAINNET) throw new Error('Custody sandbox cannot run on mainnet');
if (options.disposableSaleRehearsal && (config.database !== 'custody_test' || process.env.WIKIDECK_PG_DISPOSABLE_TEST !== '1')) {
throw new Error('Sale rehearsal requires the explicitly isolated disposable database');
}
await validateMarketPolicy(options);
if (!Number.isSafeInteger(options.feeBps) || options.feeBps < 0 || options.feeBps > 5000) throw new Error('Invalid custody fee');
const pool = new Pool(config);
// Never log connection details or let idle-client socket errors become uncaught process exceptions.
pool.on('error', () => { console.error('Custody PostgreSQL connection unavailable; requests will fail closed.'); });
try {
const c = await pool.connect();
let id: string;
try {
if (options.disposableSaleRehearsal && (await c.query('SELECT current_database() AS name')).rows[0].name !== 'custody_test') {
throw new Error('Sale rehearsal requires the explicitly isolated disposable database');
}
await c.query('BEGIN');
await c.query('SELECT pg_advisory_xact_lock($1::bigint)', [CUSTODY_WRITE_LOCK]);
await verifyCustodySchema(c);
const privilege = await c.query(`SELECT r.rolsuper,r.rolcreatedb,r.rolcreaterole,r.rolreplication,r.rolbypassrls,
pg_has_role(current_user,n.nspowner,'MEMBER') AS owns_schema
FROM pg_roles r CROSS JOIN pg_namespace n WHERE r.rolname=current_user AND n.nspname='wk_custody'`);
if (!privilege.rowCount || Object.values(privilege.rows[0]).some(Boolean)) {
throw new Error('Custody runtime requires a non-administrative role separate from the schema owner');
}
const existing = await c.query("SELECT value FROM wk_custody.custody_meta WHERE key='identity'");
if (existing.rowCount) {
const v = JSON.parse(existing.rows[0].value);
if (v.version !== 1 || v.network !== NETWORK || v.chainId !== CHAIN_ID || v.mode !== 'sandbox' ||
v.policy !== CUSTODY_POLICY || !/^[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}$/.test(v.id)) throw new Error('Custody PostgreSQL identity mismatch');
id = v.id;
} else {
id = randomUUID();
await c.query('INSERT INTO wk_custody.custody_meta VALUES($1,$2)', ['identity', JSON.stringify({ version: 1,
network: NETWORK, chainId: CHAIN_ID, mode: 'sandbox', policy: CUSTODY_POLICY, id })]);
}
await auditPostgres(c);
await c.query('COMMIT');
} catch (e) {
await c.query('ROLLBACK').catch(() => {});
if (e instanceof CustodyIntegrityError) await recordIntegrityIncident(c, e.code).catch(() => {
console.error('Custody integrity incident could not be persisted; fence all custody writers and inspect database availability.');
});
throw e;
}
finally { c.release(); }
return new PostgresCustody(pool, options, id);
} catch (e) { await pool.end(); throw e; }
}
async close(): Promise<void> { clearInterval(this.auditTimer); await this.auditInFlight?.catch(() => {}); await this.pool.end(); }
diagnostics() {
const stale = Date.now() - this.auditState.lastSuccessAt > 120000;
return { backend: 'postgres', mutationHold: this.integrityHold || this.auditState.status !== 'ok' || stale,
integrityHold: this.integrityHold, audit: { ...this.auditState, stale } };
}
info(): CustodyInfo {
return { enabled: true, mode: 'sandbox', policy: CUSTODY_POLICY, network: NETWORK, chainId: CHAIN_ID,
custodianId: this.id, feeBps: this.options.feeBps, ...resolvedMarketPolicy(this.options), liveDeposits: false, liveWithdrawals: false, nftSettlement: false };
}
private async transaction<T>(write: boolean, fn: (c: PoolClient) => Promise<T>): Promise<T> {
check(this.active < 16, 'Custody database busy; retry the original command shortly');
this.active++;
let c: PoolClient | undefined;
let mutationSavepoint = false;
try {
c = await this.pool.connect();
await c.query(write ? 'BEGIN' : 'BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY');
await c.query("SET LOCAL statement_timeout='10s'; SET LOCAL lock_timeout='3s'; SET LOCAL idle_in_transaction_session_timeout='10s'; SET LOCAL synchronous_commit=on");
// One writer per financial domain initially; other readers remain nonblocking. Lock is database-scoped,
// shared by every node/process, migrations and import, not an in-process promise queue.
if (write) {
await c.query('SELECT pg_advisory_xact_lock($1::bigint)', [CUSTODY_WRITE_LOCK]);
await c.query('SAVEPOINT custody_mutation'); mutationSavepoint = true;
}
const result = await fn(c);
await c.query('COMMIT');
return result;
} catch (e) {
if (e instanceof CustodyIntegrityError) {
this.integrityHold = true;
this.auditState = { ...this.auditState, status: 'failed', lastAttemptAt: Date.now(), failureCode: e.code };
// Roll back business changes but retain the cross-process writer lock until the incident is committed.
// Read-only/background audit failures require a separate marker transaction; DB/process loss can still
// prevent persistence, so external writer fencing remains an incident-response requirement.
if (c) {
try {
if (mutationSavepoint) await c.query('ROLLBACK TO SAVEPOINT custody_mutation');
else await c.query('ROLLBACK');
await recordIntegrityIncident(c, e.code);
if (mutationSavepoint) await c.query('COMMIT');
} catch {
await c.query('ROLLBACK').catch(() => {});
console.error('Custody integrity incident could not be persisted; fence all custody writers and inspect database availability.');
}
}
throw e;
}
await c?.query('ROLLBACK').catch(() => {});
// Never retry after an ambiguous COMMIT. Signed receipts resolve the SAME nonce on a client retry.
if (e instanceof CustodyError) throw e;
if ((e as { code?: string }).code === '23514') throw new CustodyError('Insufficient balance or custody amount constraint');
throw new Error('Custody PostgreSQL operation failed; refresh or retry the original signed command');
} finally { c?.release(); this.active--; }
}
async browse(offset = 0): Promise<CustodyPage> {
check(Number.isSafeInteger(offset) && offset >= 0 && offset <= 10000, 'Invalid custody page');
if (!resolvedMarketPolicy(this.options).marketEnabled) return { info: this.info(), listings: [], next: null };
return this.transaction(false, async c => {
const { rows } = await c.query("SELECT data FROM wk_custody.listings WHERE status IN ('open','reserved') ORDER BY sequence DESC LIMIT 25 OFFSET $1", [offset]);
return { info: this.info(), listings: rows.slice(0, 24).map(r => r.data), next: rows.length > 24 ? offset + 24 : null };
});
}
private async activity(c: PoolClient, actor: string, offset: number): Promise<CustodyActivity> {
const orders = await c.query('SELECT data FROM wk_custody.orders WHERE buyer=$1 OR seller=$1 ORDER BY sequence DESC LIMIT 25 OFFSET $2', [actor, offset]);
const withdrawals = await c.query('SELECT data FROM wk_custody.withdrawals WHERE actor=$1 ORDER BY sequence DESC LIMIT 25 OFFSET $2', [actor, offset]);
const sums = await c.query(`SELECT COALESCE(sum(balance) FILTER(WHERE id=$1),0)::text AS available,
COALESCE(sum(balance) FILTER(WHERE id LIKE $2),0)::text AS purchase,
COALESCE(sum(balance) FILTER(WHERE id LIKE $3),0)::text AS withdrawal FROM wk_custody.accounts WHERE id LIKE $4`,
[available(actor), `customer:${actor}:purchase:%`, `customer:${actor}:withdrawal:%`, `customer:${actor}:%`]);
const claimed = await c.query('SELECT 1 FROM wk_custody.credits WHERE actor=$1', [actor]);
return { availableAtomicXmr: sums.rows[0].available, purchaseReservedAtomicXmr: sums.rows[0].purchase,
withdrawalReservedAtomicXmr: sums.rows[0].withdrawal, mockCreditClaimed: !!claimed.rowCount,
orders: orders.rows.slice(0, 24).map(r => r.data), withdrawals: withdrawals.rows.slice(0, 24).map(r => r.data),
next: orders.rows.length > 24 || withdrawals.rows.length > 24 ? offset + 24 : null };
}
private async post(c: PoolClient, id: string, kind: string, lines: Line[]) {
const nonzero = lines.filter(l => (l.debit ?? 0n) !== 0n || (l.credit ?? 0n) !== 0n);
if (!nonzero.length || nonzero.reduce((s, l) => s + (l.debit ?? 0n) - (l.credit ?? 0n), 0n) !== 0n) throw new Error('Unbalanced custody posting');
await c.query('INSERT INTO wk_custody.journals(id,kind,created) VALUES($1,$2,$3)', [id, kind, (this.options.now ?? Date.now)()]);
for (const [position, line] of nonzero.entries()) {
const normal = line.normal ?? 'credit';
await c.query('INSERT INTO wk_custody.accounts(id,normal) VALUES($1,$2) ON CONFLICT(id) DO NOTHING', [line.account, normal]);
// Runtime intentionally has no UPDATE privilege on balances. The SECURITY DEFINER projection
// trigger takes the account row lock; global writer admission protects the preceding normal check.
const account = await c.query('SELECT normal FROM wk_custody.accounts WHERE id=$1', [line.account]);
if (account.rows[0].normal !== normal) throw new Error('Custody account normal mismatch');
// DB trigger updates projection; deferred trigger rejects an empty/unbalanced journal at COMMIT.
await c.query('INSERT INTO wk_custody.entries VALUES($1,$2,$3,$4,$5)',
[id, position, line.account, (line.debit ?? 0n).toString(), (line.credit ?? 0n).toString()]);
}
}
private async record<T>(c: PoolClient, table: 'listings' | 'orders' | 'withdrawals', id: string): Promise<T> {
const r = await c.query(`SELECT data FROM wk_custody.${table} WHERE id=$1 FOR UPDATE`, [id]);
check(r.rowCount, 'Custody record not found'); return r.rows[0].data as T;
}
private async update(c: PoolClient, table: 'listings' | 'orders' | 'withdrawals', record: { id: string; status: string }) {
await c.query(`UPDATE wk_custody.${table} SET status=$1,data=$2 WHERE id=$3`, [record.status, JSON.stringify(record), record.id]);
}
async execute(raw: unknown): Promise<Result> {
const cmd = custodyCommandSchema.parse(raw), message = custodyMessage(cmd), now = (this.options.now ?? Date.now)();
check(cmd.custodianId === this.id && cmd.chainId === CHAIN_ID && cmd.expiresAt >= now && cmd.expiresAt <= now + 300000 &&
verifySignature(cmd.publicKey, cmd.actor, message, cmd.signature), 'Invalid or expired custody signature');
const a = cmd.action;
if (a.type === 'activity') return this.transaction(false, c => this.activity(c, cmd.actor, a.offset));
return this.transaction(true, async c => {
const key = `${cmd.actor}:${cmd.nonce}`, hash = createHash('sha256').update(message).digest('hex');
const old = await c.query('SELECT digest,data FROM wk_custody.receipts WHERE id=$1', [key]);
if (old.rowCount) { check(old.rows[0].digest === hash, 'Nonce already used for different custody terms'); return old.rows[0].data as Result; }
const incident = await c.query("SELECT 1 FROM wk_custody.custody_meta WHERE key='integrity_incident_v1'");
check(!incident.rowCount, 'Custody integrity incident is unresolved; operator review required');
check(!this.diagnostics().mutationHold, 'Custody financial reconciliation requires operator review; balances remain readable and original receipts can be replayed');
check(permitsMarketAction(this.options, a.type), 'New market trading is disabled by this host; existing balances and resolution remain available');
// Sandbox admission budget matches SQLite. Receipt/history are NEVER pruned; cancellation stays available.
if (!['cancel-listing', 'cancel-reservation', 'cancel-withdrawal'].includes(a.type)) {
const count = await c.query('SELECT count(*)::integer AS n FROM (SELECT 1 FROM wk_custody.receipts LIMIT 10000) b');
check(count.rows[0].n < 10000, 'Custody sandbox command budget reached');
}
// Verify existing liabilities BEFORE resolving them, and validate the new business state before COMMIT.
if (a.type === 'cancel-reservation') await auditOrder(c, a.orderId);
if (a.type === 'cancel-withdrawal') await auditWithdrawal(c, a.withdrawalId);
const result = await this.mutate(c, cmd);
if (a.type === 'reserve' || a.type === 'cancel-reservation') await auditOrder(c, (result as CustodyOrder).id);
if (a.type === 'withdraw' || a.type === 'cancel-withdrawal') await auditWithdrawal(c, (result as CustodyWithdrawal).id);
await c.query('INSERT INTO wk_custody.receipts VALUES($1,$2,$3)', [key, hash, JSON.stringify(result)]);
return result;
});
}
private async mutate(c: PoolClient, cmd: CustodyCommand): Promise<Result> {
const a = cmd.action, actor = cmd.actor, createdAt = (this.options.now ?? Date.now)();
if (a.type === 'mock-credit') {
check(!(await c.query('SELECT 1 FROM wk_custody.credits WHERE actor=$1', [actor])).rowCount, 'Sandbox credit already claimed');
await this.post(c, `credit:${actor}`, 'mock-deposit', [
{ account: 'treasury:mock', normal: 'debit', debit: BigInt(MOCK_CREDIT_ATOMIC) },
{ account: available(actor), credit: BigInt(MOCK_CREDIT_ATOMIC) }]);
await c.query('INSERT INTO wk_custody.credits VALUES($1)', [actor]); return this.activity(c, actor, 0);
}
if (a.type === 'list') {
check(a.feeBps === this.options.feeBps, 'Custody hosting fee changed; review again');
const hostFeeAddress = resolvedMarketPolicy(this.options).hostFeeAddress;
check((a.hostFeeAddress ?? ADDITIONAL_MARKET_FEE.address) === hostFeeAddress, 'Host fee recipient changed; review and sign again');
const card = this.options.card(a.cardHash, actor, a.sequence);
check(card, 'Card is missing, stale, immature, or pending');
check(!(await c.query("SELECT 1 FROM wk_custody.listings WHERE card=$1 AND status IN ('open','reserved')", [a.cardHash])).rowCount, 'Card already has a custody listing');
const l: CustodyListing = { id: randomUUID(), seller: actor, cardHash: a.cardHash, sequence: a.sequence,
title: card.title, priceAtomicXmr: a.priceAtomicXmr, feeBps: a.feeBps, hostFeeAddress, status: 'open', createdAt };
await c.query('INSERT INTO wk_custody.listings(id,card,status,data) VALUES($1,$2,$3,$4)', [l.id, l.cardHash, l.status, JSON.stringify(l)]); return l;
}
if (a.type === 'reserve' || a.type === 'cancel-listing') {
const l = await this.record<CustodyListing>(c, 'listings', a.listingId);
check(l.status === 'open', 'Custody listing is no longer available');
if (a.type === 'cancel-listing') {
check(l.seller === actor, 'Only the seller can cancel this listing'); l.status = 'cancelled'; await this.update(c, 'listings', l); return l;
}
check(actor !== l.seller, 'Cannot buy your own card');
check(a.priceAtomicXmr === l.priceAtomicXmr && a.feeBps === l.feeBps, 'Custody sale terms changed');
const hostFeeAddress = l.hostFeeAddress ?? ADDITIONAL_MARKET_FEE.address;
check((a.hostFeeAddress ?? ADDITIONAL_MARKET_FEE.address) === hostFeeAddress, 'Host fee recipient does not match listing terms');
check(this.options.card(l.cardHash, l.seller, l.sequence), 'Seller card is no longer available');
const allocation = allocateXmrSale(parseAtomicXmr(a.priceAtomicXmr), a.feeBps);
const o: CustodyOrder = { id: randomUUID(), listingId: l.id, seller: l.seller, buyer: actor, cardHash: l.cardHash,
sequence: l.sequence, grossAtomicXmr: a.priceAtomicXmr, sellerAtomicXmr: allocation.sellerBeforeNetworkCosts.toString(),
authorAtomicXmr: allocation.author.toString(), hostAtomicXmr: allocation.hosting.toString(), authorAddress: ADDITIONAL_MARKET_FEE.address, hostFeeAddress,
status: 'awaiting-lock', createdAt };
await this.post(c, `reserve:${o.id}`, 'purchase-reservation', [{ account: available(actor), debit: allocation.gross }, { account: reserved(o), credit: allocation.gross }]);
l.status = 'reserved'; await this.update(c, 'listings', l);
await c.query('INSERT INTO wk_custody.orders(id,buyer,seller,status,data) VALUES($1,$2,$3,$4,$5)', [o.id, actor, o.seller, o.status, JSON.stringify(o)]);
await c.query('INSERT INTO wk_custody.outbox VALUES($1,$2,$3,$4)', [o.id, 'nft-lock', 'blocked', JSON.stringify(o)]); return o;
}
if (a.type === 'cancel-reservation') {
const o = await this.record<CustodyOrder>(c, 'orders', a.orderId);
check(!(await c.query('SELECT 1 FROM wk_custody.custody_meta WHERE key=$1', [`sale-rehearsal-binding:${o.id}`])).rowCount,
'Rehearsal sale authorization exists; cancellation requires replayed terminal evidence');
const intent = await c.query('SELECT status FROM wk_custody.outbox WHERE id=$1 FOR UPDATE', [o.id]);
check(o.buyer === actor && o.status === 'awaiting-lock' && intent.rows[0]?.status === 'blocked', 'Only an unstarted reservation can be cancelled by its buyer');
await this.post(c, `cancel:${o.id}`, 'purchase-cancellation', [{ account: reserved(o), debit: BigInt(o.grossAtomicXmr) }, { account: available(actor), credit: BigInt(o.grossAtomicXmr) }]);
o.status = 'cancelled'; await this.update(c, 'orders', o);
const l = await this.record<CustodyListing>(c, 'listings', o.listingId); l.status = 'cancelled'; await this.update(c, 'listings', l);
await c.query("UPDATE wk_custody.outbox SET status='cancelled' WHERE id=$1", [o.id]); return o;
}
if (a.type === 'withdraw') {
const w: CustodyWithdrawal = { id: randomUUID(), actor, destination: a.destination, amountAtomicXmr: a.amountAtomicXmr,
maxFeeAtomicXmr: a.maxFeeAtomicXmr, status: 'reserved', createdAt };
const total = BigInt(a.amountAtomicXmr) + BigInt(a.maxFeeAtomicXmr);
await this.post(c, `withdraw:${w.id}`, 'withdrawal-reservation', [{ account: available(actor), debit: total }, { account: held(w), credit: total }]);
await c.query('INSERT INTO wk_custody.withdrawals(id,actor,status,data) VALUES($1,$2,$3,$4)', [w.id, actor, w.status, JSON.stringify(w)]);
await c.query('INSERT INTO wk_custody.outbox VALUES($1,$2,$3,$4)', [w.id, 'withdrawal', 'blocked', JSON.stringify(w)]); return w;
}
if (a.type === 'cancel-withdrawal') {
const w = await this.record<CustodyWithdrawal>(c, 'withdrawals', a.withdrawalId);
check(w.actor === actor && w.status === 'reserved', 'Only an unprepared withdrawal can be cancelled by its owner');
const total = BigInt(w.amountAtomicXmr) + BigInt(w.maxFeeAtomicXmr);
await this.post(c, `withdraw-cancel:${w.id}`, 'withdrawal-cancellation', [{ account: held(w), debit: total }, { account: available(actor), credit: total }]);
w.status = 'cancelled'; await this.update(c, 'withdrawals', w);
await c.query("UPDATE wk_custody.outbox SET status='cancelled' WHERE id=$1", [w.id]); return w;
}
throw new CustodyError('Unsupported custody action');
}
/** Disposable database ONLY. Not wired to HTTP, configuration flags, live chain or a signer. */
observeSaleRehearsal(orderId: string, transcript: unknown) {
check(this.options.disposableSaleRehearsal === true, 'Sale rehearsal is unavailable on this custody instance');
return this.transaction(true, async c => {
check(!this.diagnostics().mutationHold, 'Custody reconciliation hold');
check(!(await c.query("SELECT 1 FROM wk_custody.custody_meta WHERE key='integrity_incident_v1'")).rowCount, 'Custody integrity incident unresolved');
const o = await this.record<CustodyOrder>(c, 'orders', orderId);
await auditOrder(c, o.id);
const l = await this.record<CustodyListing>(c, 'listings', o.listingId);
const replay = replayReservedSale(o, this.id, transcript, l.feeBps);
const key = `sale-rehearsal-binding:${o.id}`;
const old = await c.query('SELECT value FROM wk_custody.custody_meta WHERE key=$1', [key]);
if (old.rowCount) check(old.rows[0].value === replay.binding, 'Rehearsal authorization changed');
else {
check(o.status === 'awaiting-lock', 'Cannot bind an already resolved order to a rehearsal');
await c.query('INSERT INTO wk_custody.custody_meta VALUES($1,$2)', [key, replay.binding]);
}
if (replay.decision.action === 'review') throw new CustodyIntegrityError('sale-rehearsal-outcome-reorg');
// Retain the first signed observation plus terminal evidence, not a canonical event inbox.
// Runtime has INSERT but intentionally no UPDATE privilege on these metadata records.
const evidenceKey = `sale-rehearsal-observation:${o.id}`;
await c.query('INSERT INTO wk_custody.custody_meta VALUES($1,$2) ON CONFLICT(key) DO NOTHING', [evidenceKey,
JSON.stringify({ version: 1, binding: replay.binding, stateRoot: replay.stateRoot, input: replay.input })]);
if (replay.decision.action === 'settle' || replay.decision.action === 'refund') {
check(o.status === 'awaiting-lock', 'Rehearsal order already resolved');
if (replay.decision.action === 'settle') {
await this.post(c, `settle:${o.id}`, 'mock-sale-settlement', [
{ account: reserved(o), debit: BigInt(o.grossAtomicXmr) },
{ account: available(o.seller), credit: BigInt(o.sellerAtomicXmr) },
{ account: `author:${o.authorAddress}`, credit: BigInt(o.authorAtomicXmr) },
{ account: o.hostFeeAddress ? `revenue:host:${o.hostFeeAddress}` : 'revenue:host', credit: BigInt(o.hostAtomicXmr) }]);
o.status = 'settled'; l.status = 'sold';
await c.query("UPDATE wk_custody.outbox SET status='fixture-complete',data=$1 WHERE id=$2", [`fixture:${replay.stateRoot}`, o.id]);
} else {
await this.post(c, `cancel:${o.id}`, 'purchase-cancellation', [
{ account: reserved(o), debit: BigInt(o.grossAtomicXmr) }, { account: available(o.buyer), credit: BigInt(o.grossAtomicXmr) }]);
o.status = 'cancelled'; l.status = 'cancelled';
await c.query("UPDATE wk_custody.outbox SET status='cancelled' WHERE id=$1", [o.id]);
}
await this.update(c, 'orders', o); await this.update(c, 'listings', l);
await c.query('INSERT INTO wk_custody.custody_meta VALUES($1,$2)', [`sale-rehearsal-terminal:${o.id}`,
JSON.stringify({ version: 1, binding: replay.binding, stateRoot: replay.stateRoot, input: replay.input })]);
await auditOrder(c, o.id);
}
return { order: o, action: replay.decision.action, reservationDigest: replay.reservationDigest, stateRoot: replay.stateRoot };
});
}
/** Disposable observer rehearsal only; input must come from Ledger.canonicalEvents(), never peers/HTTP. */
observeCanonicalEvents(page: unknown) {
check(this.options.disposableSaleRehearsal === true, 'Canonical event inbox rehearsal is unavailable on this custody instance');
return this.transaction(true, async c => {
check(!this.diagnostics().mutationHold, 'Custody reconciliation hold');
check(!(await c.query("SELECT 1 FROM wk_custody.custody_meta WHERE key='integrity_incident_v1'")).rowCount, 'Custody integrity incident unresolved');
try { return await ingestCanonicalPage(c, page); }
catch (error) {
if (error instanceof CanonicalInboxError) throw new CustodyIntegrityError(`canonical-inbox-${error.code}`);
throw error;
}
});
}
/** Consume one bounded page directly from this process's locally validated ledger; no HTTP/peer feed. */
async observeLedgerCanonicalEvents(ledger: Ledger) {
check(this.options.disposableSaleRehearsal === true, 'Canonical event inbox rehearsal is unavailable on this custody instance');
check(ledger instanceof Ledger, 'Canonical inbox requires a local Ledger instance');
const summary = ledger.summary();
check(summary.network === NETWORK && summary.chainId === CHAIN_ID, 'Canonical ledger identity mismatch');
const cursor = await this.transaction(false, async c => {
const row = await c.query(`SELECT key FROM wk_custody.custody_meta
WHERE key LIKE 'canonical-inbox:v1:event:%' ORDER BY key DESC LIMIT 1`);
if (!row.rowCount) return 0;
const match = /^canonical-inbox:v1:event:(\d{16})$/.exec(row.rows[0].key);
if (!match) throw new CustodyIntegrityError('canonical-inbox-cursor-invalid');
return Number(match[1]);
});
let page: unknown;
try { page = ledger.canonicalEvents(cursor, 64); }
catch { throw new CustodyIntegrityError('canonical-ledger-cursor-mismatch'); }
return this.observeCanonicalEvents(page);
}
audit(): Promise<CustodyAudit> {
if (this.auditInFlight) return this.auditInFlight;
this.auditState.lastAttemptAt = Date.now();
const pending = this.transaction(false, auditPostgres).then(result => {
this.auditState = { ...this.auditState, lastSuccessAt: Date.now(),
status: this.integrityHold ? 'failed' : 'ok', failureCode: this.integrityHold ? this.auditState.failureCode : null };
return result;
}).catch(error => {
this.auditState = { ...this.auditState, status: 'failed', failureCode: error instanceof CustodyIntegrityError ? error.code : 'audit-unavailable' };
throw error;
}).finally(() => { this.auditInFlight = undefined; });
this.auditInFlight = pending; return pending;
}
importSource(): Promise<string | undefined> {
return this.transaction(false, async c => (await c.query("SELECT value FROM wk_custody.custody_meta WHERE key='import_source'")).rows[0]?.value);
}
}
/** Exact NUMERIC/text aggregation in a coherent transaction; no Number conversion of money. */
export async function auditPostgres(c: PoolClient): Promise<CustodyAudit> {
const incident = await c.query("SELECT 1 FROM wk_custody.custody_meta WHERE key='integrity_incident_v1'");
if (incident.rowCount) throw new CustodyIntegrityError('unresolved-integrity-incident');
try { await auditCanonicalInbox(c); }
catch (error) {
if (error instanceof CanonicalInboxError) throw new CustodyIntegrityError(`canonical-inbox-${error.code}`);
throw error;
}
const bad = await c.query(`SELECT 1 FROM wk_custody.journals j LEFT JOIN wk_custody.entries e ON e.journal=j.id
GROUP BY j.id HAVING count(e.position)=0 OR sum(e.debit-e.credit)<>0 LIMIT 1`);
if (bad.rowCount) throw new CustodyIntegrityError('unbalanced-journal');
const mismatch = await c.query(`SELECT 1 FROM wk_custody.accounts a LEFT JOIN wk_custody.entries e ON e.account=a.id
GROUP BY a.id HAVING a.balance<>COALESCE(sum(CASE WHEN a.normal='debit' THEN e.debit-e.credit ELSE e.credit-e.debit END),0) LIMIT 1`);
if (mismatch.rowCount) throw new CustodyIntegrityError('balance-projection-mismatch');
const totals = await c.query(`SELECT COALESCE(sum(balance) FILTER(WHERE normal='debit'),0)::text AS assets,
COALESCE(sum(balance) FILTER(WHERE normal='credit'),0)::text AS claims FROM wk_custody.accounts`);
if (totals.rows[0].assets !== totals.rows[0].claims) throw new CustodyIntegrityError('accounting-backing-mismatch');
await auditBusinessLiabilities(c);
const n = await c.query('SELECT count(*)::text AS n FROM wk_custody.journals');
if (BigInt(n.rows[0].n) > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error('Journal count exceeds supported range');
return { journals: Number(n.rows[0].n), assetsAtomicXmr: totals.rows[0].assets, claimsAndRevenueAtomicXmr: totals.rows[0].claims };
}
async function recordIntegrityIncident(c: PoolClient, code: string): Promise<void> {
await c.query('INSERT INTO wk_custody.custody_meta(key,value) VALUES($1,$2) ON CONFLICT(key) DO NOTHING',
['integrity_incident_v1', JSON.stringify({ version: 1, code, detectedAt: Date.now() })]);
}