Environment variables

This commit is contained in:
2026-10-20 16:24:01 -04:00
parent 4cac26d84b
commit 7734d8ae90
29 changed files with 267 additions and 38 deletions
+12 -4
View File
@@ -10,8 +10,9 @@ import { CUSTODY_POLICY, MOCK_CREDIT_ATOMIC, custodyCommandSchema, custodyMessag
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';
export type CustodyOptions = { feeBps: number; card(hash: string, owner: string, sequence: number): { title: string } | undefined; now?: () => number };
export type CustodyOptions = { feeBps: number; card(hash: string, owner: string, sequence: number): { title: string } | undefined; now?: () => number } & 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 };
@@ -29,6 +30,7 @@ export class PostgresCustody {
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<PostgresCustody> {
if (IS_MAINNET) throw new Error('Custody sandbox cannot run on mainnet');
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.
@@ -67,7 +69,7 @@ export class PostgresCustody {
close(): Promise<void> { 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 };
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');
@@ -93,6 +95,7 @@ export class PostgresCustody {
}
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 };
@@ -144,6 +147,7 @@ export class PostgresCustody {
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; }
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');
@@ -165,11 +169,13 @@ export class PostgresCustody {
}
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, status: 'open', createdAt };
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') {
@@ -180,11 +186,13 @@ export class PostgresCustody {
}
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,
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);