Paid-sale workflow

This commit is contained in:
2026-10-20 17:38:34 -04:00
parent 621611ecd9
commit ca8ff48628
11 changed files with 414 additions and 13 deletions
+58 -1
View File
@@ -12,8 +12,10 @@ 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';
export type CustodyOptions = { feeBps: number; card(hash: string, owner: string, sequence: number): { title: string } | undefined; now?: () => number } & MarketPolicy;
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 };
@@ -42,6 +44,9 @@ export class PostgresCustody {
}
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);
@@ -51,6 +56,9 @@ export class PostgresCustody {
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);
@@ -255,6 +263,8 @@ export class PostgresCustody {
}
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) }]);
@@ -280,6 +290,53 @@ export class PostgresCustody {
}
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 };
});
}
audit(): Promise<CustodyAudit> {
if (this.auditInFlight) return this.auditInFlight;
this.auditState.lastAttemptAt = Date.now();