import { createHash, randomUUID } from 'node:crypto'; import type { Pool, PoolClient } from 'pg'; import { z } from 'zod'; import { assertXmrRpcAmount, parseAtomicXmr } from '../../shared/xmr-amount.ts'; import { address, verifySignature } from '../testnet/consensus.ts'; import { CUSTODY_WRITE_LOCK } from './postgres-schema.ts'; const hex = z.string().regex(/^[a-f0-9]{64}$/); const fingerprint = hex; const publicKey = z.string().min(1).max(256); const signature = z.string().min(1).max(256); const uuid = z.string().uuid(); const POLICY_VERSION = 'monero-pending-credit-v1'; const MAX_EVENT_HISTORY = 256; export type DepositCreditPolicy = { /** Reviewed key-owner hashes; the signing key remains operator-held and is never stored here. */ approvedReviewerOwners: readonly string[]; /** Provisional floor; a production value still requires explicit approval. */ minimumConfirmations: number; /** Refuse credit review when observation scans have gone stale. */ maximumObservationAgeMs: number; }; export type DepositCreditReviewSubject = { policyVersion: typeof POLICY_VERSION; walletFingerprint: string; observationId: string; outputPublicKey: string; amountAtomicXmr: string; blockHeight: number; confirmations: number; minimumConfirmations: number; locked: boolean; doubleSpendSeen: boolean; allocationId: string; beneficiary: string; observationDigest: string; observationEventSequence: number; observationEventHistoryDigest: string; observerUpdatedAt: number; }; export type DepositCreditApproval = { subject: DepositCreditReviewSubject; reviewerOwner: string; reviewerPublicKey: string; nonce: string; expiresAt: number; signature: string; }; export type DepositCreditResult = { creditId: string; journalId: string; beneficiary: string; amountAtomicXmr: string; replayed: boolean }; export type DepositCreditReleaseSubject = DepositCreditReviewSubject & { creditId: string; pendingJournalId: string; unresolvedHolds: { holdId: string; eventSequence: number; eventType: 'changed' | 'not-seen' | 'reappeared' }[]; }; export type DepositCreditReleaseApproval = { subject: DepositCreditReleaseSubject; reviewerOwner: string; reviewerPublicKey: string; nonce: string; expiresAt: number; signature: string; }; export type DepositCreditReleaseResult = { creditId: string; releaseId: string; journalId: string; beneficiary: string; amountAtomicXmr: string; replayed: boolean }; export type DepositReorgResolutionSubject = { policyVersion: 'monero-reorg-resolution-v1'; creditId: string; releaseId: string; holdId: string; walletFingerprint: string; observationId: string; eventSequence: number; eventType: 'changed' | 'not-seen' | 'reappeared'; currentObservation: DepositCreditReviewSubject; }; export type DepositReorgResolutionApproval = { subject: DepositReorgResolutionSubject; reviewerOwner: string; reviewerPublicKey: string; nonce: string; expiresAt: number; signature: string; }; export type DepositReorgResolutionResult = { resolutionId: string; holdId: string; replayed: boolean }; const subjectSchema = z.object({ policyVersion: z.literal(POLICY_VERSION), walletFingerprint: fingerprint, observationId: z.string().min(129).max(160), outputPublicKey: hex, amountAtomicXmr: z.string().regex(/^[1-9]\d{0,19}$/), blockHeight: z.number().int().positive().safe(), confirmations: z.number().int().positive().safe(), minimumConfirmations: z.number().int().min(10).max(100_000), locked: z.boolean(), doubleSpendSeen: z.boolean(), allocationId: uuid, beneficiary: hex, observationDigest: hex, observationEventSequence: z.number().int().nonnegative().safe(), observationEventHistoryDigest: hex, observerUpdatedAt: z.number().int().nonnegative().safe(), }).strict(); const approvalSchema = z.object({ subject: subjectSchema, reviewerOwner: hex, reviewerPublicKey: publicKey, nonce: uuid, expiresAt: z.number().int().nonnegative().safe(), signature }).strict(); const releaseSubjectSchema = subjectSchema.extend({ creditId: uuid, pendingJournalId: z.string().min(1).max(256), unresolvedHolds: z.array(z.object({ holdId: uuid, eventSequence: z.number().int().positive().safe(), eventType: z.enum(['changed', 'not-seen', 'reappeared']) }).strict()).max(1024) }).strict(); const releaseApprovalSchema = z.object({ subject: releaseSubjectSchema, reviewerOwner: hex, reviewerPublicKey: publicKey, nonce: uuid, expiresAt: z.number().int().nonnegative().safe(), signature }).strict(); const resolutionSubjectSchema = z.object({ policyVersion: z.literal('monero-reorg-resolution-v1'), creditId: uuid, releaseId: uuid, holdId: uuid, walletFingerprint: fingerprint, observationId: z.string().min(129).max(160), eventSequence: z.number().int().positive().safe(), eventType: z.enum(['changed','not-seen','reappeared']), currentObservation: subjectSchema }).strict(); const resolutionApprovalSchema = z.object({ subject: resolutionSubjectSchema, reviewerOwner: hex, reviewerPublicKey: publicKey, nonce: uuid, expiresAt: z.number().int().nonnegative().safe(), signature }).strict(); function digest(value: string): string { return createHash('sha256').update(value).digest('hex'); } function canonicalReviewMessage(subject: DepositCreditReviewSubject, reviewerOwner: string, nonce: string, expiresAt: number): string { return JSON.stringify(['wikideck-monero-pending-credit-review-v1', reviewerOwner, nonce, expiresAt, subject.policyVersion, subject.walletFingerprint, subject.observationId, subject.outputPublicKey, subject.amountAtomicXmr, subject.blockHeight, subject.confirmations, subject.minimumConfirmations, subject.locked, subject.doubleSpendSeen, subject.allocationId, subject.beneficiary, subject.observationDigest, subject.observationEventSequence, subject.observationEventHistoryDigest, subject.observerUpdatedAt]); } function canonicalReleaseMessage(subject: DepositCreditReleaseSubject, reviewerOwner: string, nonce: string, expiresAt: number): string { return JSON.stringify(['wikideck-monero-pending-release-v1', reviewerOwner, nonce, expiresAt, subject.policyVersion, subject.creditId, subject.pendingJournalId, subject.walletFingerprint, subject.observationId, subject.outputPublicKey, subject.amountAtomicXmr, subject.blockHeight, subject.confirmations, subject.minimumConfirmations, subject.locked, subject.doubleSpendSeen, subject.allocationId, subject.beneficiary, subject.observationDigest, subject.observationEventSequence, subject.observationEventHistoryDigest, subject.observerUpdatedAt, subject.unresolvedHolds.map(hold => [hold.holdId, hold.eventSequence, hold.eventType])]); } function canonicalResolutionMessage(subject: DepositReorgResolutionSubject, reviewerOwner: string, nonce: string, expiresAt: number): string { const observation = subject.currentObservation; return JSON.stringify(['wikideck-monero-reorg-resolution-v1', reviewerOwner, nonce, expiresAt, subject.policyVersion, subject.creditId, subject.releaseId, subject.holdId, subject.walletFingerprint, subject.observationId, subject.eventSequence, subject.eventType, observation.outputPublicKey, observation.amountAtomicXmr, observation.blockHeight, observation.confirmations, observation.minimumConfirmations, observation.locked, observation.doubleSpendSeen, observation.allocationId, observation.beneficiary, observation.observationDigest, observation.observationEventSequence, observation.observationEventHistoryDigest, observation.observerUpdatedAt]); } function validatePolicy(policy: DepositCreditPolicy): void { if (!Array.isArray(policy.approvedReviewerOwners) || !policy.approvedReviewerOwners.length || policy.approvedReviewerOwners.length > 8 || policy.approvedReviewerOwners.some(owner => !hex.safeParse(owner).success) || !Number.isSafeInteger(policy.minimumConfirmations) || policy.minimumConfirmations < 10 || policy.minimumConfirmations > 100_000 || !Number.isSafeInteger(policy.maximumObservationAgeMs) || policy.maximumObservationAgeMs < 10_000 || policy.maximumObservationAgeMs > 15 * 60_000) { throw new Error('Invalid private Monero deposit-credit policy'); } } async function readReviewSubject(client: PoolClient, walletFingerprint: string, observationId: string, policy: DepositCreditPolicy, now: number, allowReorgHistory = false): Promise { const observation = await client.query(`SELECT o.wallet_fingerprint,o.observation_id,o.txid,o.monero_address,o.account_index, o.address_index,o.output_public_key,o.amount_atomic_xmr::text,o.block_height,o.confirmations,o.locked, o.double_spend_seen,o.payload_digest,o.currently_present,s.updated_at AS observer_updated_at FROM wk_custody.monero_transfer_observations o JOIN wk_custody.monero_observer_state s USING(wallet_fingerprint) WHERE o.wallet_fingerprint=$1 AND o.observation_id=$2`, [walletFingerprint, observationId]); if (observation.rowCount !== 1) throw new Error('Deposit observation not found'); const row = observation.rows[0]; if (!row.output_public_key || row.observation_id !== `${row.txid}:${row.output_public_key}` || !row.currently_present || row.double_spend_seen || row.locked || Number(row.confirmations) < policy.minimumConfirmations) { throw new Error('Deposit observation is not eligible for reviewed pending credit'); } if (now - Number(row.observer_updated_at) < 0 || now - Number(row.observer_updated_at) > policy.maximumObservationAgeMs) { throw new Error('Monero observer state is stale'); } const amount = BigInt(row.amount_atomic_xmr); assertXmrRpcAmount(amount); parseAtomicXmr(amount.toString()); const allocation = await client.query(`SELECT allocation_id,beneficiary,custodian_id,market_network,market_chain_id FROM wk_custody.monero_deposit_allocations WHERE wallet_fingerprint=$1 AND account_index=$2 AND address_index=$3 AND monero_address=$4 AND status='registered'`, [walletFingerprint, row.account_index, row.address_index, row.monero_address]); if (allocation.rowCount !== 1) throw new Error('Deposit output has no unique registered beneficiary allocation'); const identity = await client.query("SELECT value FROM wk_custody.custody_meta WHERE key='identity'"); if (identity.rowCount !== 1) throw new Error('Custodian identity is unavailable'); const custodian = JSON.parse(identity.rows[0].value) as { id?: string; network?: string; chainId?: string; mode?: string; policy?: string }; const mapped = allocation.rows[0]; const validIdentity = (custodian.mode === 'sandbox' && custodian.network === 'wikideck-randomx-testnet-6' && custodian.policy === 'custody-sandbox-v1' && mapped.market_network === 'wikideck-randomx-testnet-6') || (custodian.mode === 'mainnet-custody' && custodian.network === 'wikideck-randomx-mainnet-1' && custodian.policy === 'custody-mainnet-v1' && mapped.market_network === 'wikideck-randomx-mainnet-1'); if (!validIdentity || mapped.custodian_id !== custodian.id || mapped.market_network !== custodian.network || mapped.market_chain_id !== custodian.chainId) throw new Error('Deposit allocation is outside this custody identity'); const events = await client.query(`SELECT sequence,event_type,payload_digest,wallet_height,observed_at FROM wk_custody.monero_observation_events WHERE wallet_fingerprint=$1 AND observation_id=$2 ORDER BY sequence LIMIT $3`, [walletFingerprint, observationId, MAX_EVENT_HISTORY + 1]); if ((events.rowCount ?? events.rows.length) > MAX_EVENT_HISTORY) throw new Error('Deposit observation event history exceeds review bound'); if (!allowReorgHistory && events.rows.some(event => event.event_type === 'not-seen' || event.event_type === 'reappeared')) { throw new Error('Deposit observation has prior disappearance/reappearance and requires separate review'); } const sequence = events.rows.length ? Number(events.rows.at(-1)!.sequence) : 0; const eventDigest = digest(JSON.stringify(events.rows.map(event => [Number(event.sequence), event.event_type, event.payload_digest, Number(event.wallet_height), Number(event.observed_at)]))); return { policyVersion: POLICY_VERSION, walletFingerprint, observationId, outputPublicKey: row.output_public_key, amountAtomicXmr: amount.toString(), blockHeight: Number(row.block_height), confirmations: Number(row.confirmations), minimumConfirmations: policy.minimumConfirmations, locked: row.locked, doubleSpendSeen: row.double_spend_seen, allocationId: mapped.allocation_id, beneficiary: mapped.beneficiary, observationDigest: row.payload_digest, observationEventSequence: sequence, observationEventHistoryDigest: eventDigest, observerUpdatedAt: Number(row.observer_updated_at), }; } /** Return the exact current snapshot an authorized operator must review and sign. */ export async function prepareDepositCreditReview(pool: Pool, walletFingerprint: string, observationId: string, policy: DepositCreditPolicy, now = Date.now()): Promise { validatePolicy(policy); fingerprint.parse(walletFingerprint); const c = await pool.connect(); try { await c.query('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'"); const subject = await readReviewSubject(c, walletFingerprint, observationId, policy, now); await c.query('COMMIT'); return subject; } catch (error) { await c.query('ROLLBACK').catch(() => {}); throw error; } finally { c.release(); } } /** Canonical message an allow-listed reviewer signs after examining the exact output/allocation snapshot. */ export function depositCreditApprovalMessage(subject: DepositCreditReviewSubject, reviewerOwner: string, nonce: string, expiresAt: number): string { return canonicalReviewMessage(subjectSchema.parse(subject), hex.parse(reviewerOwner), uuid.parse(nonce), expiresAt); } /** * Posts an operator-reviewed credit into a non-spendable pending-liability account. This service is not * wired to HTTP/startup; release to available balance is a separate, not-yet-implemented approval step. */ export async function postReviewedDepositPendingCredit(pool: Pool, rawApproval: DepositCreditApproval, policy: DepositCreditPolicy, now = Date.now()): Promise { validatePolicy(policy); const approval = approvalSchema.parse(rawApproval); if (!policy.approvedReviewerOwners.includes(approval.reviewerOwner) || address(approval.reviewerPublicKey) !== approval.reviewerOwner || !verifySignature(approval.reviewerPublicKey, approval.reviewerOwner, canonicalReviewMessage(approval.subject, approval.reviewerOwner, approval.nonce, approval.expiresAt), approval.signature)) { throw new Error('Deposit credit review signature is invalid'); } const reviewDigest = digest(canonicalReviewMessage(approval.subject, approval.reviewerOwner, approval.nonce, approval.expiresAt)); const client = await pool.connect(); try { await client.query('BEGIN'); await client.query("SET LOCAL statement_timeout='10s'; SET LOCAL lock_timeout='3s'; SET LOCAL idle_in_transaction_session_timeout='10s'; SET LOCAL synchronous_commit=on"); await client.query('SELECT pg_advisory_xact_lock($1::bigint)', [CUSTODY_WRITE_LOCK]); const existing = await client.query(`SELECT credit_id,journal_id,beneficiary,amount_atomic_xmr::text,reviewer_owner, review_nonce,observation_digest,allocation_id::text,reviewer_public_key,review_expires_at,review_signature, minimum_confirmations,review_digest FROM wk_custody.monero_deposit_credit_events WHERE wallet_fingerprint=$1 AND observation_id=$2`, [approval.subject.walletFingerprint, approval.subject.observationId]); if (existing.rowCount) { const saved = existing.rows[0]; if (saved.reviewer_owner !== approval.reviewerOwner || saved.review_nonce !== approval.nonce || saved.observation_digest !== approval.subject.observationDigest || saved.amount_atomic_xmr !== approval.subject.amountAtomicXmr || saved.allocation_id !== approval.subject.allocationId || saved.minimum_confirmations !== approval.subject.minimumConfirmations || saved.reviewer_public_key !== approval.reviewerPublicKey || Number(saved.review_expires_at) !== approval.expiresAt || saved.review_signature !== approval.signature || saved.review_digest !== reviewDigest) { throw new Error('Deposit output was already staged under different reviewed terms'); } await client.query('COMMIT'); return { creditId: saved.credit_id, journalId: saved.journal_id, beneficiary: saved.beneficiary, amountAtomicXmr: saved.amount_atomic_xmr, replayed: true }; } if (approval.expiresAt < now || approval.expiresAt > now + 5 * 60_000) { throw new Error('Deposit credit review signature is expired or outside the allowed window'); } const current = await readReviewSubject(client, approval.subject.walletFingerprint, approval.subject.observationId, policy, now); if (JSON.stringify(current) !== JSON.stringify(approval.subject)) throw new Error('Deposit observation changed after reviewer approval'); const creditId = randomUUID(); const journalId = `monero-deposit-pending:${digest(`${current.walletFingerprint}:${current.observationId}`)}`; const assetAccount = 'treasury:monero:pending'; const liabilityAccount = `customer:${current.beneficiary}:deposit-pending:${creditId}`; await client.query('INSERT INTO wk_custody.journals(id,kind,created) VALUES($1,$2,$3)', [journalId, 'monero-deposit-pending-credit', now]); const lines = [[0, assetAccount, 'debit'], [1, liabilityAccount, 'credit']] as const; for (const [position, accountId, normal] of lines) { await client.query('INSERT INTO wk_custody.accounts(id,normal) VALUES($1,$2) ON CONFLICT(id) DO NOTHING', [accountId, normal]); const account = await client.query('SELECT normal FROM wk_custody.accounts WHERE id=$1', [accountId]); if (account.rows[0]?.normal !== normal) throw new Error('Deposit pending account normal mismatch'); await client.query('INSERT INTO wk_custody.entries VALUES($1,$2,$3,$4,$5)', [journalId, position, accountId, normal === 'debit' ? current.amountAtomicXmr : '0', normal === 'credit' ? current.amountAtomicXmr : '0']); } await client.query(`INSERT INTO wk_custody.monero_deposit_credit_events( credit_id,wallet_fingerprint,observation_id,allocation_id,beneficiary,amount_atomic_xmr,journal_id, observation_digest,last_observation_event_sequence,reviewer_owner,reviewer_public_key,review_nonce, review_expires_at,review_signature,review_subject,review_digest,credited_at,minimum_confirmations) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18)`, [creditId, current.walletFingerprint, current.observationId, current.allocationId, current.beneficiary, current.amountAtomicXmr, journalId, current.observationDigest, current.observationEventSequence, approval.reviewerOwner, approval.reviewerPublicKey, approval.nonce, approval.expiresAt, approval.signature, JSON.stringify(current), reviewDigest, now, current.minimumConfirmations]); await client.query('COMMIT'); return { creditId, journalId, beneficiary: current.beneficiary, amountAtomicXmr: current.amountAtomicXmr, replayed: false }; } catch (error) { await client.query('ROLLBACK').catch(() => {}); throw error; } finally { client.release(); } } async function readReleaseSubject(client: PoolClient, creditId: string, policy: DepositCreditPolicy, now: number): Promise { const credit = await client.query(`SELECT credit_id::text,wallet_fingerprint,observation_id,beneficiary, amount_atomic_xmr::text,journal_id FROM wk_custody.monero_deposit_credit_events WHERE credit_id=$1`, [creditId]); if (credit.rowCount !== 1) throw new Error('Pending deposit credit not found'); const row = credit.rows[0]; if (await client.query('SELECT 1 FROM wk_custody.monero_deposit_release_events WHERE credit_id=$1', [creditId]).then(r => r.rowCount)) { throw new Error('Pending deposit credit was already released'); } const observation = await readReviewSubject(client, row.wallet_fingerprint, row.observation_id, policy, now, true); if (observation.beneficiary !== row.beneficiary || observation.amountAtomicXmr !== row.amount_atomic_xmr) { throw new Error('Pending credit no longer matches the current observed output'); } const holds = await client.query(`SELECT h.hold_id::text,h.observation_event_sequence::text,h.event_type FROM wk_custody.monero_deposit_reorg_holds h WHERE h.credit_id=$1 AND NOT EXISTS ( SELECT 1 FROM wk_custody.monero_deposit_reorg_resolutions r WHERE r.hold_id=h.hold_id) ORDER BY h.observation_event_sequence LIMIT 1025`, [creditId]); if (holds.rows.length > 1024) throw new Error('Deposit reorg hold set exceeds review bound'); return { ...observation, creditId, pendingJournalId: row.journal_id, unresolvedHolds: holds.rows.map(hold => ({ holdId: hold.hold_id, eventSequence: Number(hold.observation_event_sequence), eventType: hold.event_type })) }; } /** Return the current pending liability and unresolved reorg evidence for explicit reviewer approval. */ export async function prepareDepositCreditReleaseReview(pool: Pool, creditId: string, policy: DepositCreditPolicy, now = Date.now()): Promise { validatePolicy(policy); uuid.parse(creditId); const client = await pool.connect(); try { await client.query('BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY'); await client.query("SET LOCAL statement_timeout='10s'; SET LOCAL lock_timeout='3s'; SET LOCAL idle_in_transaction_session_timeout='10s'"); const subject = await readReleaseSubject(client, creditId, policy, now); await client.query('COMMIT'); return subject; } catch (error) { await client.query('ROLLBACK').catch(() => {}); throw error; } finally { client.release(); } } /** Canonical reviewer message for releasing a pending liability and resolving its included scoped holds. */ export function depositCreditReleaseApprovalMessage(subject: DepositCreditReleaseSubject, reviewerOwner: string, nonce: string, expiresAt: number): string { return canonicalReleaseMessage(releaseSubjectSchema.parse(subject), hex.parse(reviewerOwner), uuid.parse(nonce), expiresAt); } /** Explicit reviewer-authorized, idempotent move from pending to spendable available balance. Not HTTP-wired. */ export async function releaseReviewedDepositCredit(pool: Pool, rawApproval: DepositCreditReleaseApproval, policy: DepositCreditPolicy, now = Date.now()): Promise { validatePolicy(policy); const approval = releaseApprovalSchema.parse(rawApproval); const message = canonicalReleaseMessage(approval.subject, approval.reviewerOwner, approval.nonce, approval.expiresAt); if (!policy.approvedReviewerOwners.includes(approval.reviewerOwner) || address(approval.reviewerPublicKey) !== approval.reviewerOwner || !verifySignature(approval.reviewerPublicKey, approval.reviewerOwner, message, approval.signature)) { throw new Error('Deposit release review signature is invalid'); } const reviewDigest = digest(message); const client = await pool.connect(); try { await client.query('BEGIN'); await client.query("SET LOCAL statement_timeout='10s'; SET LOCAL lock_timeout='3s'; SET LOCAL idle_in_transaction_session_timeout='10s'; SET LOCAL synchronous_commit=on"); await client.query('SELECT pg_advisory_xact_lock($1::bigint)', [CUSTODY_WRITE_LOCK]); const existing = await client.query(`SELECT release_id,credit_id::text,journal_id,reviewer_owner,review_nonce, review_signature,review_digest,review_subject,released_at FROM wk_custody.monero_deposit_release_events WHERE credit_id=$1`, [approval.subject.creditId]); if (existing.rowCount) { const saved = existing.rows[0]; if (saved.reviewer_owner !== approval.reviewerOwner || saved.review_nonce !== approval.nonce || saved.review_signature !== approval.signature || saved.review_digest !== reviewDigest) { throw new Error('Deposit release was already committed under different reviewed terms'); } const credit = await client.query(`SELECT beneficiary,amount_atomic_xmr::text FROM wk_custody.monero_deposit_credit_events WHERE credit_id=$1`, [saved.credit_id]); await client.query('COMMIT'); return { creditId: saved.credit_id, releaseId: saved.release_id, journalId: saved.journal_id, beneficiary: credit.rows[0].beneficiary, amountAtomicXmr: credit.rows[0].amount_atomic_xmr, replayed: true }; } if (approval.expiresAt < now || approval.expiresAt > now + 5 * 60_000) { throw new Error('Deposit release review signature is expired or outside the allowed window'); } const current = await readReleaseSubject(client, approval.subject.creditId, policy, now); if (JSON.stringify(current) !== JSON.stringify(approval.subject)) throw new Error('Deposit release evidence changed after reviewer approval'); const pendingAccount = `customer:${current.beneficiary}:deposit-pending:${current.creditId}`; const availableAccount = `customer:${current.beneficiary}:available`; const pending = await client.query('SELECT normal FROM wk_custody.accounts WHERE id=$1', [pendingAccount]); if (pending.rowCount !== 1 || pending.rows[0].normal !== 'credit') { throw new Error('Pending deposit liability does not match reviewed release amount'); } await client.query('INSERT INTO wk_custody.accounts(id,normal) VALUES($1,\'credit\') ON CONFLICT(id) DO NOTHING', [availableAccount]); const available = await client.query('SELECT normal FROM wk_custody.accounts WHERE id=$1', [availableAccount]); if (available.rows[0]?.normal !== 'credit') throw new Error('Available customer account normal mismatch'); const releaseId = randomUUID(), journalId = `monero-deposit-release:${current.creditId}`; await client.query('INSERT INTO wk_custody.journals(id,kind,created) VALUES($1,$2,$3)', [journalId, 'monero-deposit-release', now]); await client.query('INSERT INTO wk_custody.entries VALUES($1,0,$2,$3,0),($1,1,$4,0,$3)', [journalId, pendingAccount, current.amountAtomicXmr, availableAccount]); await client.query(`INSERT INTO wk_custody.monero_deposit_release_events( release_id,credit_id,journal_id,reviewer_owner,reviewer_public_key,review_nonce,review_expires_at, review_signature,review_subject,review_digest,released_at) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11)`, [releaseId, current.creditId, journalId, approval.reviewerOwner, approval.reviewerPublicKey, approval.nonce, approval.expiresAt, approval.signature, JSON.stringify(current), reviewDigest, now]); for (const hold of current.unresolvedHolds) { await client.query(`INSERT INTO wk_custody.monero_deposit_reorg_resolutions( resolution_id,hold_id,credit_id,release_id,resolved_by,resolved_at,disposition,resolution_kind, reviewer_public_key,review_nonce,review_expires_at,review_signature,review_subject,review_digest) VALUES($1,$2,$3,$4,$5,$6,'revalidated-for-release','release-review',$7,$8,$9,$10,$11,$12)`, [randomUUID(), hold.holdId, current.creditId, releaseId, approval.reviewerOwner, now, approval.reviewerPublicKey, approval.nonce, approval.expiresAt, approval.signature, JSON.stringify({ subject: current, holdId: hold.holdId }), reviewDigest]); } await client.query('COMMIT'); return { creditId: current.creditId, releaseId, journalId, beneficiary: current.beneficiary, amountAtomicXmr: current.amountAtomicXmr, replayed: false }; } catch (error) { await client.query('ROLLBACK').catch(() => {}); throw error; } finally { client.release(); } } async function readReorgResolutionSubject(client: PoolClient, creditId: string, holdId: string, policy: DepositCreditPolicy, now: number): Promise { const row = await client.query(`SELECT h.hold_id::text,h.credit_id::text,h.wallet_fingerprint,h.observation_id, h.observation_event_sequence::text,h.event_type,c.release_id::text FROM wk_custody.monero_deposit_reorg_holds h JOIN wk_custody.monero_deposit_release_events c ON c.credit_id=h.credit_id WHERE h.hold_id=$1 AND h.credit_id=$2 AND NOT EXISTS( SELECT 1 FROM wk_custody.monero_deposit_reorg_resolutions r WHERE r.hold_id=h.hold_id)`, [holdId, creditId]); if (row.rowCount !== 1) throw new Error('Unresolved released-deposit reorg hold not found'); const currentObservation = await readReviewSubject(client, row.rows[0].wallet_fingerprint, row.rows[0].observation_id, policy, now, true); return resolutionSubjectSchema.parse({ policyVersion: 'monero-reorg-resolution-v1', creditId, releaseId: row.rows[0].release_id, holdId, walletFingerprint: row.rows[0].wallet_fingerprint, observationId: row.rows[0].observation_id, eventSequence: Number(row.rows[0].observation_event_sequence), eventType: row.rows[0].event_type, currentObservation }); } /** Prepare an exact current snapshot to resolve one scoped reorg hold after the output reappears. */ export async function prepareDepositReorgResolution(pool: Pool, creditId: string, holdId: string, policy: DepositCreditPolicy, now = Date.now()): Promise { validatePolicy(policy); uuid.parse(creditId); uuid.parse(holdId); const client = await pool.connect(); try { await client.query('BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY'); await client.query("SET LOCAL statement_timeout='10s'; SET LOCAL lock_timeout='3s'; SET LOCAL idle_in_transaction_session_timeout='10s'"); const subject = await readReorgResolutionSubject(client, creditId, holdId, policy, now); await client.query('COMMIT'); return subject; } catch (error) { await client.query('ROLLBACK').catch(() => {}); throw error; } finally { client.release(); } } export function depositReorgResolutionMessage(subject: DepositReorgResolutionSubject, reviewerOwner: string, nonce: string, expiresAt: number): string { return canonicalResolutionMessage(resolutionSubjectSchema.parse(subject), hex.parse(reviewerOwner), uuid.parse(nonce), expiresAt); } /** Resolve a reorg hold only after the output is again present, unlocked, sufficiently confirmed, and signed off. */ export async function resolveReleasedDepositReorgHold(pool: Pool, rawApproval: DepositReorgResolutionApproval, policy: DepositCreditPolicy, now = Date.now()): Promise { validatePolicy(policy); const approval = resolutionApprovalSchema.parse(rawApproval); const message = canonicalResolutionMessage(approval.subject, approval.reviewerOwner, approval.nonce, approval.expiresAt); if (!policy.approvedReviewerOwners.includes(approval.reviewerOwner) || address(approval.reviewerPublicKey) !== approval.reviewerOwner || !verifySignature(approval.reviewerPublicKey, approval.reviewerOwner, message, approval.signature)) { throw new Error('Deposit reorg resolution signature is invalid'); } const reviewDigest = digest(message), client = await pool.connect(); try { await client.query('BEGIN'); await client.query("SET LOCAL statement_timeout='10s'; SET LOCAL lock_timeout='3s'; SET LOCAL synchronous_commit=on"); await client.query('SELECT pg_advisory_xact_lock($1::bigint)', [CUSTODY_WRITE_LOCK]); const existing = await client.query(`SELECT resolution_id::text,review_nonce,review_signature,review_digest FROM wk_custody.monero_deposit_reorg_resolutions WHERE hold_id=$1`, [approval.subject.holdId]); if (existing.rowCount) { const saved = existing.rows[0]; if (saved.review_nonce !== approval.nonce || saved.review_signature !== approval.signature || saved.review_digest !== reviewDigest) { throw new Error('Deposit reorg hold was already resolved under different reviewed terms'); } await client.query('COMMIT'); return { resolutionId: saved.resolution_id, holdId: approval.subject.holdId, replayed: true }; } if (approval.expiresAt < now || approval.expiresAt > now + 5 * 60_000) { throw new Error('Deposit reorg resolution signature is expired or outside the allowed window'); } const current = await readReorgResolutionSubject(client, approval.subject.creditId, approval.subject.holdId, policy, now); if (JSON.stringify(current) !== JSON.stringify(approval.subject)) throw new Error('Deposit reorg evidence changed after review'); const resolutionId = randomUUID(); await client.query(`INSERT INTO wk_custody.monero_deposit_reorg_resolutions( resolution_id,hold_id,credit_id,release_id,resolved_by,resolved_at,disposition,resolution_kind, reviewer_public_key,review_nonce,review_expires_at,review_signature,review_subject,review_digest) VALUES($1,$2,$3,$4,$5,$6,'revalidated-for-release','post-release-review',$7,$8,$9,$10,$11,$12)`, [resolutionId, approval.subject.holdId, approval.subject.creditId, approval.subject.releaseId, approval.reviewerOwner, now, approval.reviewerPublicKey, approval.nonce, approval.expiresAt, approval.signature, JSON.stringify(approval.subject), reviewDigest]); await client.query('COMMIT'); return { resolutionId, holdId: approval.subject.holdId, replayed: false }; } catch (error) { await client.query('ROLLBACK').catch(() => {}); throw error; } finally { client.release(); } } /** Append review holds for post-credit Monero reorg signals; this never reverses a journal or releases pending funds. */ export async function reconcileDepositCreditReorgHolds(pool: Pool, walletFingerprint: string, now = Date.now()): Promise { fingerprint.parse(walletFingerprint); const client = await pool.connect(); try { await client.query('BEGIN'); await client.query("SET LOCAL statement_timeout='10s'; SET LOCAL lock_timeout='3s'; SET LOCAL synchronous_commit=on"); await client.query('SELECT pg_advisory_xact_lock($1::bigint)', [CUSTODY_WRITE_LOCK]); const reorgs = await client.query(`SELECT c.credit_id,c.observation_id,e.sequence,e.event_type FROM wk_custody.monero_deposit_credit_events c JOIN wk_custody.monero_observation_events e ON e.wallet_fingerprint=c.wallet_fingerprint AND e.observation_id=c.observation_id AND e.sequence>c.last_observation_event_sequence WHERE c.wallet_fingerprint=$1 AND e.event_type IN ('changed','not-seen','reappeared') AND NOT EXISTS (SELECT 1 FROM wk_custody.monero_deposit_reorg_holds h WHERE h.credit_id=c.credit_id AND h.observation_event_sequence=e.sequence) ORDER BY e.sequence LIMIT 1024`, [walletFingerprint]); for (const row of reorgs.rows) { await client.query(`INSERT INTO wk_custody.monero_deposit_reorg_holds( hold_id,credit_id,wallet_fingerprint,observation_id,observation_event_sequence,event_type,created_at) VALUES($1,$2,$3,$4,$5,$6,$7) ON CONFLICT(credit_id,observation_event_sequence) DO NOTHING`, [randomUUID(), row.credit_id, walletFingerprint, row.observation_id, row.sequence, row.event_type, now]); } await client.query('COMMIT'); return reorgs.rowCount ?? 0; } catch (error) { await client.query('ROLLBACK').catch(() => {}); throw error; } finally { client.release(); } }