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 { 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'; export type CustodyOptions = { feeBps: number; card(hash: string, owner: string, sequence: number): { title: string } | undefined; now?: () => number }; 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 constructor(pool: Pool, options: CustodyOptions, id: string) { this.pool = pool; this.options = options; this.id = id; } static async open(config: PoolConfig, options: CustodyOptions): Promise { if (IS_MAINNET) throw new Error('Custody sandbox cannot run on mainnet'); 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 { 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(() => {}); throw e; } finally { c.release(); } return new PostgresCustody(pool, options, id); } catch (e) { await pool.end(); throw e; } } close(): Promise { return this.pool.end(); } info(): CustodyInfo { return { enabled: true, mode: 'sandbox', policy: CUSTODY_POLICY, network: NETWORK, chainId: CHAIN_ID, custodianId: this.id, feeBps: this.options.feeBps, liveDeposits: false, liveWithdrawals: false, nftSettlement: false }; } private async transaction(write: boolean, fn: (c: PoolClient) => Promise): Promise { check(this.active < 16, 'Custody database busy; retry the original command shortly'); this.active++; let c: PoolClient | undefined; 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]); const result = await fn(c); await c.query('COMMIT'); return result; } catch (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 { check(Number.isSafeInteger(offset) && offset >= 0 && offset <= 10000, 'Invalid custody page'); 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 { 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(c: PoolClient, table: 'listings' | 'orders' | 'withdrawals', id: string): Promise { 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 { 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; } // 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'); } const result = await this.mutate(c, cmd); 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 { 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 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, 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(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'); 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, 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(c, 'orders', a.orderId); 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(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(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'); } audit(): Promise { return this.transaction(false, auditPostgres); } importSource(): Promise { 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 { 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 Error('Unbalanced PostgreSQL custody history'); 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 Error('PostgreSQL custody 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 Error('PostgreSQL custody backing mismatch'); 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 }; }