import { DatabaseSync } from 'node:sqlite'; import { createHash, randomUUID } from 'node:crypto'; import { mkdir, chmod } from 'node:fs/promises'; import { join } from 'node:path'; import { IS_MAINNET } from '../../shared/network.ts'; import { NETWORK } from '../../shared/testnet.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 CustodyCommand, type CustodyActivity, type CustodyInfo, type CustodyListing, type CustodyOrder, type CustodyWithdrawal, type CustodyPage } from '../../shared/custody.ts'; import { CHAIN_ID, verifySignature } from '../testnet/consensus.ts'; import { resolvedMarketPolicy, validateMarketPolicy, permitsMarketAction, type MarketPolicy } from './policy.ts'; export function custodyMode(env: NodeJS.ProcessEnv = process.env): 'off' | 'sandbox' { const mode = env.WIKIDECK_CUSTODY_MODE ?? 'off'; if (mode !== 'off' && mode !== 'sandbox') throw new Error('Custody supports off or sandbox only; live custody is disabled'); if (IS_MAINNET && mode !== 'off') throw new Error('Custody sandbox cannot run on mainnet'); return mode; } export class CustodyError extends Error {} type Card = { title: string }; type Options = { feeBps: number; card(cardHash: string, owner: string, sequence: number): Card | undefined; now?: () => number } & MarketPolicy; type Account = { id: string; normal: 'debit' | 'credit'; balance: string }; type Line = { account: string; normal: 'debit' | 'credit'; debit?: bigint; credit?: bigint }; type Result = CustodyActivity | CustodyListing | CustodyOrder | CustodyWithdrawal; const AGGREGATE_MAX = 10n ** 30n - 1n; const LIMIT = 10000; // Explicit disposable-sandbox resource budget; never archive financial history silently. const available = (actor: string) => `customer:${actor}:available`; const orderAccount = (order: CustodyOrder) => `customer:${order.buyer}:purchase:${order.id}`; const withdrawalAccount = (w: CustodyWithdrawal) => `customer:${w.actor}:withdrawal:${w.id}`; const debit = (account: string, amount: bigint, normal: 'debit' | 'credit' = 'credit'): Line => ({ account, normal, debit: amount }); const credit = (account: string, amount: bigint, normal: 'debit' | 'credit' = 'credit'): Line => ({ account, normal, credit: amount }); const digest = (value: string) => createHash('sha256').update(value).digest('hex'); function requireState(condition: unknown, message: string): asserts condition { if (!condition) throw new CustodyError(message); } /** Isolated, durable OFFLINE/SANDBOX financial model. No wallet RPC, keys, addresses or NFT signer. */ export class CustodyMarket { private readonly db: DatabaseSync; private readonly options: Options; readonly id: string; private readonly now: () => number; private constructor(db: DatabaseSync, options: Options, id: string) { this.db = db; this.options = options; this.id = id; this.now = options.now ?? Date.now; } static async open(directory: string, options: Options): Promise { 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 folder = join(directory, 'testnet'); await mkdir(folder, { recursive: true, mode: 0o700 }); const file = join(folder, 'custody-sandbox.sqlite'); const db = new DatabaseSync(file); try { db.exec(`PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL; PRAGMA busy_timeout=5000; PRAGMA foreign_keys=ON; CREATE TABLE IF NOT EXISTS custody_meta(key TEXT PRIMARY KEY,value TEXT NOT NULL); CREATE TABLE IF NOT EXISTS accounts(id TEXT PRIMARY KEY,normal TEXT NOT NULL CHECK(normal IN ('debit','credit')),balance TEXT NOT NULL); CREATE TABLE IF NOT EXISTS journals(id TEXT PRIMARY KEY,kind TEXT NOT NULL,created INTEGER NOT NULL); CREATE TABLE IF NOT EXISTS entries(journal TEXT NOT NULL REFERENCES journals(id),position INTEGER NOT NULL, account TEXT NOT NULL REFERENCES accounts(id),debit TEXT NOT NULL,credit TEXT NOT NULL,PRIMARY KEY(journal,position)); CREATE TABLE IF NOT EXISTS listings(id TEXT PRIMARY KEY,card TEXT NOT NULL,status TEXT NOT NULL,data TEXT NOT NULL); CREATE UNIQUE INDEX IF NOT EXISTS custody_one_card ON listings(card) WHERE status IN ('open','reserved'); CREATE TABLE IF NOT EXISTS orders(id TEXT PRIMARY KEY,buyer TEXT NOT NULL,seller TEXT NOT NULL,status TEXT NOT NULL,data TEXT NOT NULL); CREATE INDEX IF NOT EXISTS custody_orders_buyer ON orders(buyer); CREATE INDEX IF NOT EXISTS custody_orders_seller ON orders(seller); CREATE TABLE IF NOT EXISTS withdrawals(id TEXT PRIMARY KEY,actor TEXT NOT NULL,status TEXT NOT NULL,data TEXT NOT NULL); CREATE INDEX IF NOT EXISTS custody_withdrawals_actor ON withdrawals(actor); CREATE TABLE IF NOT EXISTS receipts(id TEXT PRIMARY KEY,digest TEXT NOT NULL,data TEXT NOT NULL); CREATE TABLE IF NOT EXISTS outbox(id TEXT PRIMARY KEY,kind TEXT NOT NULL,status TEXT NOT NULL,data TEXT NOT NULL); CREATE TABLE IF NOT EXISTS credits(actor TEXT PRIMARY KEY); CREATE TRIGGER IF NOT EXISTS entries_no_update BEFORE UPDATE ON entries BEGIN SELECT RAISE(ABORT,'Immutable financial entry'); END; CREATE TRIGGER IF NOT EXISTS entries_no_delete BEFORE DELETE ON entries BEGIN SELECT RAISE(ABORT,'Immutable financial entry'); END; CREATE TRIGGER IF NOT EXISTS journals_no_update BEFORE UPDATE ON journals BEGIN SELECT RAISE(ABORT,'Immutable financial journal'); END; CREATE TRIGGER IF NOT EXISTS journals_no_delete BEFORE DELETE ON journals BEGIN SELECT RAISE(ABORT,'Immutable financial journal'); END;`); db.exec('BEGIN IMMEDIATE'); let id: string; try { if (db.prepare("SELECT 1 FROM custody_meta WHERE key='postgres_cutover'").get()) { throw new Error('SQLite custody was frozen for PostgreSQL cutover; resume the import, never restart SQLite'); } const row = db.prepare('SELECT value FROM custody_meta WHERE key=?').get('identity') as { value: string } | undefined; if (row) { const identity = JSON.parse(row.value); if (identity.version !== 1 || identity.network !== NETWORK || identity.chainId !== CHAIN_ID || identity.mode !== 'sandbox' || identity.policy !== CUSTODY_POLICY || !/^[a-f0-9-]{36}$/.test(identity.id)) throw new Error('Custody identity/schema mismatch'); id = identity.id; } else { id = randomUUID(); db.prepare('INSERT INTO custody_meta VALUES(?,?)').run('identity', JSON.stringify({ version: 1, mode: 'sandbox', policy: CUSTODY_POLICY, network: NETWORK, chainId: CHAIN_ID, id })); } db.exec('COMMIT'); } catch (error) { db.exec('ROLLBACK'); throw error; } if (process.platform !== 'win32') await chmod(file, 0o600); const market = new CustodyMarket(db, options, id); market.audit(); // Refuse to start if projections no longer match the immutable journal. return market; } catch (error) { db.close(); throw error; } } close() { this.db.close(); } 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 atomic(fn: () => T): T { this.db.exec('BEGIN IMMEDIATE'); try { const result = fn(); this.db.exec('COMMIT'); return result; } catch (error) { this.db.exec('ROLLBACK'); throw error; } } private balance(id: string): bigint { const row = this.db.prepare('SELECT balance FROM accounts WHERE id=?').get(id) as { balance: string } | undefined; return row ? BigInt(row.balance) : 0n; } private post(id: string, kind: string, lines: Line[]) { if (['mock-deposit', 'purchase-reservation', 'withdrawal-reservation'].includes(kind)) { requireState((this.db.prepare('SELECT COUNT(*) AS n FROM journals').get() as { n: number }).n < LIMIT, 'Custody sandbox journal budget reached; existing reservations can still be cancelled'); } const totals = lines.reduce((s, line) => s + (line.debit ?? 0n) - (line.credit ?? 0n), 0n); if (totals !== 0n || !lines.length) throw new Error('Unbalanced custody journal'); this.db.prepare('INSERT INTO journals VALUES(?,?,?)').run(id, kind, this.now()); for (const [position, line] of lines.entries()) { const d = line.debit ?? 0n, c = line.credit ?? 0n; if (d < 0n || c < 0n || (d > 0n && c > 0n)) throw new Error('Invalid journal line'); if (d === 0n && c === 0n) continue; this.db.prepare('INSERT OR IGNORE INTO accounts VALUES(?,?,?)').run(line.account, line.normal, '0'); const account = this.db.prepare('SELECT * FROM accounts WHERE id=?').get(line.account) as Account; if (account.normal !== line.normal) throw new Error('Account normal changed'); const balance = BigInt(account.balance) + (line.normal === 'debit' ? d - c : c - d); requireState(balance >= 0n, 'Insufficient available sandbox balance'); if (balance > AGGREGATE_MAX) throw new CustodyError('Custody aggregate amount limit reached'); this.db.prepare('INSERT INTO entries VALUES(?,?,?,?,?)').run(id, position, line.account, d.toString(), c.toString()); this.db.prepare('UPDATE accounts SET balance=? WHERE id=?').run(balance.toString(), line.account); } } private get(table: 'listings' | 'orders' | 'withdrawals', id: string): T { const row = this.db.prepare(`SELECT data FROM ${table} WHERE id=?`).get(id) as { data: string } | undefined; requireState(row, 'Custody record not found'); return JSON.parse(row.data) as T; } private saveOrder(order: CustodyOrder) { this.db.prepare('INSERT INTO orders VALUES(?,?,?,?,?) ON CONFLICT(id) DO UPDATE SET status=excluded.status,data=excluded.data') .run(order.id, order.buyer, order.seller, order.status, JSON.stringify(order)); } private saveWithdrawal(w: CustodyWithdrawal) { this.db.prepare('INSERT INTO withdrawals VALUES(?,?,?,?) ON CONFLICT(id) DO UPDATE SET status=excluded.status,data=excluded.data') .run(w.id, w.actor, w.status, JSON.stringify(w)); } private saveListing(l: CustodyListing) { this.db.prepare('INSERT INTO listings VALUES(?,?,?,?) ON CONFLICT(id) DO UPDATE SET status=excluded.status,data=excluded.data') .run(l.id, l.cardHash, l.status, JSON.stringify(l)); } browse(offset = 0): CustodyPage { requireState(Number.isSafeInteger(offset) && offset >= 0 && offset <= LIMIT, 'Invalid custody page'); if (!resolvedMarketPolicy(this.options).marketEnabled) return { info: this.info(), listings: [], next: null }; const rows = this.db.prepare("SELECT data FROM listings WHERE status IN ('open','reserved') ORDER BY rowid DESC LIMIT 25 OFFSET ?") .all(offset) as { data: string }[]; return { info: this.info(), listings: rows.slice(0, 24).map(r => JSON.parse(r.data)), next: rows.length > 24 ? offset + 24 : null }; } private activity(actor: string, offset: number): CustodyActivity { const orders = this.db.prepare('SELECT data FROM orders WHERE buyer=? OR seller=? ORDER BY rowid DESC LIMIT 25 OFFSET ?') .all(actor, actor, offset) as { data: string }[]; const withdrawals = this.db.prepare('SELECT data FROM withdrawals WHERE actor=? ORDER BY rowid DESC LIMIT 25 OFFSET ?') .all(actor, offset) as { data: string }[]; const sum = (prefix: string) => (this.db.prepare('SELECT balance FROM accounts WHERE id GLOB ?').all(prefix) as { balance: string }[]) .reduce((s, r) => s + BigInt(r.balance), 0n).toString(); return { availableAtomicXmr: this.balance(available(actor)).toString(), purchaseReservedAtomicXmr: sum(`customer:${actor}:purchase:*`), withdrawalReservedAtomicXmr: sum(`customer:${actor}:withdrawal:*`), mockCreditClaimed: !!this.db.prepare('SELECT 1 FROM credits WHERE actor=?').get(actor), orders: orders.slice(0, 24).map(r => JSON.parse(r.data)), withdrawals: withdrawals.slice(0, 24).map(r => JSON.parse(r.data)), next: orders.length > 24 || withdrawals.length > 24 ? offset + 24 : null }; } execute(raw: unknown): Result { const c = custodyCommandSchema.parse(raw), message = custodyMessage(c), now = this.now(); requireState(c.network === NETWORK && c.chainId === CHAIN_ID && c.custodianId === this.id && c.expiresAt >= now && c.expiresAt <= now + 5 * 60_000 && verifySignature(c.publicKey, c.actor, message, c.signature), 'Invalid or expired custody signature'); if (c.action.type === 'activity') return this.activity(c.actor, c.action.offset); return this.atomic(() => { const key = `${c.actor}:${c.nonce}`, hash = digest(message); const prior = this.db.prepare('SELECT digest,data FROM receipts WHERE id=?').get(key) as { digest: string; data: string } | undefined; if (prior) { requireState(prior.digest === hash, 'Nonce already used for different custody terms'); return JSON.parse(prior.data) as Result; } requireState(permitsMarketAction(this.options, c.action.type), 'New market trading is disabled by this host; existing balances and resolution remain available'); if (!['cancel-listing', 'cancel-reservation', 'cancel-withdrawal'].includes(c.action.type)) { requireState((this.db.prepare('SELECT COUNT(*) AS n FROM receipts').get() as { n: number }).n < LIMIT, 'Custody sandbox command budget reached; existing reservations can still be cancelled'); } const result = this.mutate(c); this.db.prepare('INSERT INTO receipts VALUES(?,?,?)').run(key, hash, JSON.stringify(result)); return result; }); } private mutate(c: CustodyCommand): Result { const a = c.action; if (a.type === 'mock-credit') { requireState(!this.db.prepare('SELECT 1 FROM credits WHERE actor=?').get(c.actor), 'Sandbox credit already claimed'); this.post(`credit:${c.actor}`, 'mock-deposit', [debit('treasury:mock', BigInt(MOCK_CREDIT_ATOMIC), 'debit'), credit(available(c.actor), BigInt(MOCK_CREDIT_ATOMIC))]); this.db.prepare('INSERT INTO credits VALUES(?)').run(c.actor); return this.activity(c.actor, 0); } if (a.type === 'list') { requireState(a.feeBps === this.options.feeBps, 'Custody hosting fee changed; review again'); const hostFeeAddress = resolvedMarketPolicy(this.options).hostFeeAddress; requireState((a.hostFeeAddress ?? ADDITIONAL_MARKET_FEE.address) === hostFeeAddress, 'Host fee recipient changed; review and sign again'); const card = this.options.card(a.cardHash, c.actor, a.sequence); requireState(card, 'Card is missing, stale, immature, or pending'); requireState(!this.db.prepare("SELECT 1 FROM listings WHERE card=? AND status IN ('open','reserved')").get(a.cardHash), 'Card already has a custody listing'); const l: CustodyListing = { id: randomUUID(), seller: c.actor, cardHash: a.cardHash, sequence: a.sequence, title: card.title, priceAtomicXmr: a.priceAtomicXmr, feeBps: a.feeBps, hostFeeAddress, status: 'open', createdAt: this.now() }; this.saveListing(l); return l; } if (a.type === 'cancel-listing' || a.type === 'reserve') { const l = this.get('listings', a.listingId); requireState(l.status === 'open', 'Custody listing is no longer available'); if (a.type === 'cancel-listing') { requireState(l.seller === c.actor, 'Only the seller can cancel this listing'); l.status = 'cancelled'; this.saveListing(l); return l; } requireState(c.actor !== l.seller, 'Cannot buy your own card'); requireState(l.priceAtomicXmr === a.priceAtomicXmr && l.feeBps === a.feeBps, 'Custody sale terms changed'); const hostFeeAddress = l.hostFeeAddress ?? ADDITIONAL_MARKET_FEE.address; requireState((a.hostFeeAddress ?? ADDITIONAL_MARKET_FEE.address) === hostFeeAddress, 'Host fee recipient does not match listing terms'); requireState(this.options.card(l.cardHash, l.seller, l.sequence), 'Seller card is no longer available'); const allocation = allocateXmrSale(parseAtomicXmr(a.priceAtomicXmr), a.feeBps); const order: CustodyOrder = { id: randomUUID(), listingId: l.id, seller: l.seller, buyer: c.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: this.now() }; this.post(`reserve:${order.id}`, 'purchase-reservation', [debit(available(c.actor), allocation.gross), credit(orderAccount(order), allocation.gross)]); l.status = 'reserved'; this.saveListing(l); this.saveOrder(order); // Durable intent only. No NFT signing/submission worker is enabled until reviewed consensus rules exist. this.db.prepare('INSERT INTO outbox VALUES(?,?,?,?)').run(order.id, 'nft-lock', 'blocked', JSON.stringify(order)); return order; } if (a.type === 'cancel-reservation') { const o = this.get('orders', a.orderId); requireState(o.buyer === c.actor && o.status === 'awaiting-lock', 'Only the buyer can cancel an unstarted sandbox reservation'); const intent = this.db.prepare('SELECT status FROM outbox WHERE id=?').get(o.id) as { status: string } | undefined; requireState(intent?.status === 'blocked', 'NFT intent may have executed; reservation requires review'); // Safe only because this sandbox never emits a custody delivery witness. Do not reuse for live refunds. this.post(`cancel:${o.id}`, 'purchase-cancellation', [debit(orderAccount(o), BigInt(o.grossAtomicXmr)), credit(available(o.buyer), BigInt(o.grossAtomicXmr))]); o.status = 'cancelled'; this.saveOrder(o); const l = this.get('listings', o.listingId); l.status = 'cancelled'; this.saveListing(l); this.db.prepare("UPDATE outbox SET status='cancelled' WHERE id=?").run(o.id); return o; } if (a.type === 'withdraw') { const w: CustodyWithdrawal = { id: randomUUID(), actor: c.actor, destination: a.destination, amountAtomicXmr: a.amountAtomicXmr, maxFeeAtomicXmr: a.maxFeeAtomicXmr, status: 'reserved', createdAt: this.now() }; const total = parseAtomicXmr(w.amountAtomicXmr) + parseAtomicXmr(w.maxFeeAtomicXmr); this.post(`withdraw:${w.id}`, 'withdrawal-reservation', [debit(available(c.actor), total), credit(withdrawalAccount(w), total)]); this.saveWithdrawal(w); this.db.prepare('INSERT INTO outbox VALUES(?,?,?,?)').run(w.id, 'withdrawal', 'blocked', JSON.stringify(w)); return w; } if (a.type === 'cancel-withdrawal') { const w = this.get('withdrawals', a.withdrawalId); requireState(w.actor === c.actor && w.status === 'reserved', 'Only an unprepared withdrawal can be cancelled by its owner'); const total = BigInt(w.amountAtomicXmr) + BigInt(w.maxFeeAtomicXmr); this.post(`withdraw-cancel:${w.id}`, 'withdrawal-cancellation', [debit(withdrawalAccount(w), total), credit(available(c.actor), total)]); w.status = 'cancelled'; this.saveWithdrawal(w); this.db.prepare("UPDATE outbox SET status='cancelled' WHERE id=?").run(w.id); return w; } throw new CustodyError('Unsupported custody action'); } /** Offline fixture seam ONLY: records a simulated canonical event, never reachable from HTTP. */ settleFixture(orderId: string, evidenceId: string): CustodyOrder { requireState(/^fixture:[a-f0-9]{64}$/.test(evidenceId), 'Expected offline fixture evidence'); return this.atomic(() => { const o = this.get('orders', orderId); if (o.status === 'settled') { const event = this.db.prepare('SELECT data FROM outbox WHERE id=?').get(o.id) as { data: string }; requireState(event.data === evidenceId, 'Conflicting fixture settlement evidence'); return o; } requireState(o.status === 'awaiting-lock', 'Order is not eligible for fixture settlement'); this.post(`settle:${o.id}`, 'mock-sale-settlement', [debit(orderAccount(o), BigInt(o.grossAtomicXmr)), credit(available(o.seller), BigInt(o.sellerAtomicXmr)), credit(`author:${o.authorAddress}`, BigInt(o.authorAtomicXmr)), credit(o.hostFeeAddress ? `revenue:host:${o.hostFeeAddress}` : 'revenue:host', BigInt(o.hostAtomicXmr))]); o.status = 'settled'; this.saveOrder(o); const l = this.get('listings', o.listingId); l.status = 'sold'; this.saveListing(l); this.db.prepare("UPDATE outbox SET status='fixture-complete',data=? WHERE id=?").run(evidenceId, o.id); return o; }); } /** Offline mock treasury: persist one transaction identity; a retry must never prepare another spend. */ prepareWithdrawalFixture(id: string, attemptId: string, transactionId: string, feeAtomicXmr: string): CustodyWithdrawal { requireState(/^fixture:[a-f0-9]{64}$/.test(transactionId) && /^[a-f0-9-]{36}$/.test(attemptId), 'Expected offline withdrawal identity'); const fee = parseAtomicXmr(feeAtomicXmr); return this.atomic(() => { const w = this.get('withdrawals', id); if (w.status === 'prepared' || w.status === 'complete') { requireState(w.attemptId === attemptId && w.transactionId === transactionId && w.feeAtomicXmr === feeAtomicXmr, 'Withdrawal already prepared with different terms'); return w; } requireState(w.status === 'reserved' && fee <= BigInt(w.maxFeeAtomicXmr), 'Withdrawal state or fee cap invalid'); w.status = 'prepared'; w.attemptId = attemptId; w.transactionId = transactionId; w.feeAtomicXmr = feeAtomicXmr; this.saveWithdrawal(w); this.db.prepare("UPDATE outbox SET status='fixture-prepared',data=? WHERE id=?").run(JSON.stringify(w), w.id); return w; }); } confirmWithdrawalFixture(id: string, transactionId: string): CustodyWithdrawal { return this.atomic(() => { const w = this.get('withdrawals', id); requireState(w.transactionId === transactionId && ['prepared', 'complete'].includes(w.status), 'Withdrawal identity/state mismatch'); if (w.status === 'complete') return w; const amount = BigInt(w.amountAtomicXmr), fee = BigInt(w.feeAtomicXmr!), maxFee = BigInt(w.maxFeeAtomicXmr); this.post(`withdraw-confirm:${id}`, 'mock-withdrawal', [debit(withdrawalAccount(w), amount + maxFee), credit('treasury:mock', amount + fee, 'debit'), credit(available(w.actor), maxFee - fee)]); w.status = 'complete'; this.saveWithdrawal(w); this.db.prepare("UPDATE outbox SET status='fixture-complete' WHERE id=?").run(w.id); return w; }); } /** No floating SQL SUM: replay canonical strings using bigint, independently of balance projections. */ audit(): { journals: number; assetsAtomicXmr: string; claimsAndRevenueAtomicXmr: string } { return this.atomic(() => { const accounts = this.db.prepare('SELECT * FROM accounts').all() as Account[]; const balances = new Map(accounts.map(a => [a.id, 0n])); const normals = new Map(accounts.map(a => [a.id, a.normal])); const journals = this.db.prepare('SELECT id FROM journals').all() as { id: string }[]; for (const journal of journals) { const lines = this.db.prepare('SELECT account,debit,credit FROM entries WHERE journal=? ORDER BY position').all(journal.id) as { account: string; debit: string; credit: string }[]; let total = 0n; if (!lines.length) throw new Error('Empty custody journal'); for (const l of lines) { if (!/^(0|[1-9]\d{0,29})$/.test(l.debit) || !/^(0|[1-9]\d{0,29})$/.test(l.credit)) throw new Error('Invalid custody entry encoding'); const d = BigInt(l.debit), c = BigInt(l.credit); if (d > 0n && c > 0n || !normals.has(l.account)) throw new Error('Invalid custody entry'); total += d - c; balances.set(l.account, balances.get(l.account)! + (normals.get(l.account) === 'debit' ? d - c : c - d)); } if (total !== 0n) throw new Error('Unbalanced custody history'); } let assets = 0n, claims = 0n; for (const a of accounts) { if (!/^(0|[1-9]\d{0,29})$/.test(a.balance) || balances.get(a.id) !== BigInt(a.balance)) throw new Error('Custody balance projection mismatch'); if (a.normal === 'debit') assets += BigInt(a.balance); else claims += BigInt(a.balance); } if (assets !== claims) throw new Error('Custody backing mismatch'); return { journals: journals.length, assetsAtomicXmr: assets.toString(), claimsAndRevenueAtomicXmr: claims.toString() }; }); } }