import { createHash } from 'node:crypto'; import type { Pool, PoolClient } from 'pg'; import { address, verifySignature } from '../testnet/consensus.ts'; import { validateXmrAddress } from './xmr.ts'; import { assertXmrRpcAmount } from '../../shared/xmr-amount.ts'; import { CUSTODY_WRITE_LOCK } from './postgres-schema.ts'; import { mainnetWithdrawalCancellationMessage, mainnetWithdrawalCancellationSchema, mainnetCustodyBalanceMessage, mainnetCustodyBalanceRequestSchema, mainnetWithdrawalMessage, mainnetWithdrawalRequestSchema } from '../../shared/monero-withdrawal.ts'; const MAX_REQUEST_LIFETIME_MS = 5 * 60_000; const digest = (text: string) => createHash('sha256').update(text).digest('hex'); const availableAccount = (beneficiary: string) => `customer:${beneficiary}:available`; const withdrawalAccount = (beneficiary: string, requestId: string) => `customer:${beneficiary}:monero-withdrawal:${requestId}`; export type MainnetWithdrawalIntent = { requestId: string; custodianId: string; beneficiary: string; destination: string; amountAtomicXmr: string; maxFeeAtomicXmr: string; status: 'reserved' | 'cancelled'; createdAt: number; cancelledAt: number | null; }; export type MainnetCustodyBalance = { availableAtomicXmr: string; depositPendingAtomicXmr: string; withdrawalReservedAtomicXmr: string; }; async function insertJournal(client: PoolClient, journalId: string, kind: string, createdAt: number, debitAccount: string, creditAccount: string, amount: string): Promise { for (const [accountId, normal] of [[debitAccount, 'credit'], [creditAccount, 'credit']] as const) { await client.query('INSERT INTO wk_custody.accounts(id,normal) VALUES($1,$2) ON CONFLICT(id) DO NOTHING', [accountId, normal]); const row = await client.query('SELECT normal FROM wk_custody.accounts WHERE id=$1', [accountId]); if (row.rowCount !== 1 || row.rows[0].normal !== normal) throw new Error('Mainnet withdrawal ledger account mismatch'); } await client.query('INSERT INTO wk_custody.journals(id,kind,created) VALUES($1,$2,$3)', [journalId, kind, createdAt]); await client.query('INSERT INTO wk_custody.entries VALUES($1,0,$2,$3,0),($1,1,$4,0,$3)', [journalId, debitAccount, amount, creditAccount]); } async function mainnetIdentity(client: PoolClient, custodianId: string, chainId: string): Promise { const result = await client.query("SELECT value FROM wk_custody.custody_meta WHERE key='identity'"); if (result.rowCount !== 1) throw new Error('Mainnet custody identity is not initialized'); const identity = JSON.parse(result.rows[0].value) as Record; if (identity.version !== 1 || identity.mode !== 'mainnet-custody' || identity.policy !== 'custody-mainnet-v1' || identity.network !== 'wikideck-randomx-mainnet-1' || identity.id !== custodianId || identity.chainId !== chainId) { throw new Error('Withdrawal request does not match the initialized mainnet custody identity'); } } function expiryIsValid(expiresAt: number, now: number): boolean { return Number.isSafeInteger(now) && now >= 0 && expiresAt >= now && expiresAt <= now + MAX_REQUEST_LIFETIME_MS; } /** Customer-signed request: atomically reserve available liability, but do not prepare or send XMR. */ export async function reserveMainnetWithdrawal(pool: Pool, rawRequest: unknown, now = Date.now()): Promise { const request = mainnetWithdrawalRequestSchema.parse(rawRequest); const destination = await validateXmrAddress(request.destination); if (address(request.publicKey) !== request.beneficiary || !verifySignature(request.publicKey, request.beneficiary, mainnetWithdrawalMessage(request), request.signature)) { throw new Error('Mainnet withdrawal signature is invalid'); } const requestDigest = digest(mainnetWithdrawalMessage(request)); const amount = BigInt(request.amountAtomicXmr), fee = BigInt(request.maxFeeAtomicXmr), total = amount + fee; assertXmrRpcAmount(amount); assertXmrRpcAmount(fee); 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]); await mainnetIdentity(client, request.custodianId, request.chainId); const prior = await client.query('SELECT * FROM wk_custody.monero_withdrawal_intents WHERE request_id=$1 FOR UPDATE', [request.requestId]); if (prior.rowCount) { const row = prior.rows[0]; if (row.request_digest !== requestDigest || row.request_signature !== request.signature) throw new Error('Withdrawal request ID was reused with different terms'); await client.query('COMMIT'); return { requestId: row.request_id, custodianId: row.custodian_id, beneficiary: row.beneficiary, destination: row.destination, amountAtomicXmr: row.amount_atomic_xmr, maxFeeAtomicXmr: row.max_fee_atomic_xmr, status: row.status, createdAt: Number(row.created_at), cancelledAt: row.cancelled_at === null ? null : Number(row.cancelled_at) }; } if (!expiryIsValid(request.expiresAt, now)) throw new Error('Mainnet withdrawal request is expired or outside the allowed window'); const hold = await client.query(`SELECT 1 FROM wk_custody.monero_deposit_credit_events d JOIN wk_custody.monero_deposit_reorg_holds h ON h.credit_id=d.credit_id LEFT JOIN wk_custody.monero_deposit_reorg_resolutions r ON r.hold_id=h.hold_id WHERE d.beneficiary=$1 AND r.hold_id IS NULL LIMIT 1`, [request.beneficiary]); if (hold.rowCount) throw new Error('Customer deposit is under reorg review; new withdrawals are held'); const creditTotal = await client.query(`SELECT COALESCE(sum(amount_atomic_xmr),0)::text AS credited FROM wk_custody.monero_deposit_release_events r JOIN wk_custody.monero_deposit_credit_events c ON c.credit_id=r.credit_id WHERE c.beneficiary=$1`, [request.beneficiary]); const activeTotals = await client.query(`SELECT COALESCE((SELECT sum(amount_atomic_xmr+max_fee_atomic_xmr) FROM wk_custody.monero_withdrawal_intents WHERE beneficiary=$1 AND status='reserved'),0)::text AS reserved`, [request.beneficiary]); const available = await client.query('SELECT balance::text,normal FROM wk_custody.accounts WHERE id=$1', [availableAccount(request.beneficiary)]); if (available.rowCount !== 1 || available.rows[0].normal !== 'credit' || BigInt(available.rows[0].balance) < total) { throw new Error('Insufficient available mainnet custody balance'); } const alreadyReserved = BigInt(activeTotals.rows[0].reserved); const backed = BigInt(creditTotal.rows[0].credited); if (alreadyReserved + total > backed) throw new Error('Mainnet customer withdrawals exceed reviewed released deposit backing'); const createdAt = now, reserveJournal = `monero-withdrawal-reserve:${request.requestId}`; await insertJournal(client, reserveJournal, 'monero-withdrawal-reservation', now, availableAccount(request.beneficiary), withdrawalAccount(request.beneficiary, request.requestId), total.toString()); await client.query(`INSERT INTO wk_custody.monero_withdrawal_intents( request_id,custodian_id,market_network,market_chain_id,beneficiary,destination,amount_atomic_xmr,max_fee_atomic_xmr, public_key,expires_at,request_signature,request_digest,status,created_at) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,'reserved',$13)`, [request.requestId, request.custodianId, request.network, request.chainId, request.beneficiary, destination, request.amountAtomicXmr, request.maxFeeAtomicXmr, request.publicKey, request.expiresAt, request.signature, requestDigest, createdAt]); await client.query('COMMIT'); return { requestId: request.requestId, custodianId: request.custodianId, beneficiary: request.beneficiary, destination, amountAtomicXmr: request.amountAtomicXmr, maxFeeAtomicXmr: request.maxFeeAtomicXmr, status: 'reserved', createdAt, cancelledAt: null }; } catch (error) { await client.query('ROLLBACK').catch(() => {}); throw error; } finally { client.release(); } } /** Customer may release a reservation only before any separate treasury preparation has occurred. */ export async function cancelMainnetWithdrawal(pool: Pool, rawRequest: unknown, now = Date.now()): Promise { const cancellation = mainnetWithdrawalCancellationSchema.parse(rawRequest); if (address(cancellation.publicKey) !== cancellation.beneficiary || !verifySignature(cancellation.publicKey, cancellation.beneficiary, mainnetWithdrawalCancellationMessage(cancellation), cancellation.signature)) { throw new Error('Mainnet withdrawal cancellation signature is invalid'); } 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]); await mainnetIdentity(client, cancellation.custodianId, cancellation.chainId); const result = await client.query('SELECT * FROM wk_custody.monero_withdrawal_intents WHERE request_id=$1 FOR UPDATE', [cancellation.withdrawalRequestId]); if (result.rowCount !== 1 || result.rows[0].beneficiary !== cancellation.beneficiary) { throw new Error('Mainnet withdrawal request not found for this beneficiary'); } const row = result.rows[0]; if (row.status === 'cancelled') { if (row.cancel_request_id !== cancellation.cancellationId || row.cancel_signature !== cancellation.signature) { throw new Error('Withdrawal was cancelled under different signed terms'); } await client.query('COMMIT'); return { requestId: row.request_id, custodianId: row.custodian_id, beneficiary: row.beneficiary, destination: row.destination, amountAtomicXmr: row.amount_atomic_xmr, maxFeeAtomicXmr: row.max_fee_atomic_xmr, status: 'cancelled', createdAt: Number(row.created_at), cancelledAt: Number(row.cancelled_at) }; } if (!expiryIsValid(cancellation.expiresAt, now)) throw new Error('Mainnet withdrawal cancellation is expired or outside the allowed window'); const amount = BigInt(row.amount_atomic_xmr), maxFee = BigInt(row.max_fee_atomic_xmr), total = amount + maxFee; const cancelJournal = `monero-withdrawal-cancel:${row.request_id}`; await insertJournal(client, cancelJournal, 'monero-withdrawal-cancellation', now, withdrawalAccount(row.beneficiary, row.request_id), availableAccount(row.beneficiary), total.toString()); await client.query(`UPDATE wk_custody.monero_withdrawal_intents SET status='cancelled',cancel_request_id=$2, cancel_public_key=$3,cancel_expires_at=$4,cancel_signature=$5,cancelled_at=$6 WHERE request_id=$1`, [row.request_id, cancellation.cancellationId, cancellation.publicKey, cancellation.expiresAt, cancellation.signature, now]); await client.query('COMMIT'); return { requestId: row.request_id, custodianId: row.custodian_id, beneficiary: row.beneficiary, destination: row.destination, amountAtomicXmr: row.amount_atomic_xmr, maxFeeAtomicXmr: row.max_fee_atomic_xmr, status: 'cancelled', createdAt: Number(row.created_at), cancelledAt: now }; } catch (error) { await client.query('ROLLBACK').catch(() => {}); throw error; } finally { client.release(); } } /** Read the caller's own balances using a signed, expiring request; never accepts an actor from query parameters. */ export async function readMainnetCustodyBalance(pool: Pool, rawRequest: unknown, now = Date.now()): Promise { const request = mainnetCustodyBalanceRequestSchema.parse(rawRequest); if (address(request.publicKey) !== request.beneficiary || !verifySignature(request.publicKey, request.beneficiary, mainnetCustodyBalanceMessage(request), request.signature)) { throw new Error('Mainnet custody balance signature is invalid'); } if (!expiryIsValid(request.expiresAt, now)) throw new Error('Mainnet custody balance request is expired or outside the allowed window'); 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'"); await mainnetIdentity(client, request.custodianId, request.chainId); const available = await client.query('SELECT balance::text FROM wk_custody.accounts WHERE id=$1', [availableAccount(request.beneficiary)]); const pending = await client.query(`SELECT COALESCE(sum(balance),0)::text AS balance FROM wk_custody.accounts WHERE id LIKE $1`, [`customer:${request.beneficiary}:deposit-pending:%`]); const withdrawals = await client.query(`SELECT COALESCE(sum(amount_atomic_xmr+max_fee_atomic_xmr),0)::text AS balance FROM wk_custody.monero_withdrawal_intents WHERE beneficiary=$1 AND status='reserved'`, [request.beneficiary]); await client.query('COMMIT'); return { availableAtomicXmr: available.rows[0]?.balance ?? '0', depositPendingAtomicXmr: pending.rows[0].balance, withdrawalReservedAtomicXmr: withdrawals.rows[0].balance }; } catch (error) { await client.query('ROLLBACK').catch(() => {}); throw error; } finally { client.release(); } }