import { z } from 'zod'; import { NETWORK } from './testnet.ts'; import { ADDITIONAL_MARKET_FEE } from './market.ts'; import { parseAtomicXmr } from './xmr-amount.ts'; const hex = z.string().regex(/^[a-f0-9]{64}$/); const id = z.string().uuid(); const amount = z.string().max(22).refine(value => { try { return parseAtomicXmr(value) > 0n; } catch { return false; } }, 'Expected a positive canonical atomic XMR amount'); const nonnegativeAmount = z.string().max(22).refine(value => { try { parseAtomicXmr(value); return true; } catch { return false; } }, 'Expected canonical atomic XMR amount'); export const CUSTODY_POLICY = 'custody-sandbox-v1'; export const MOCK_CREDIT_ATOMIC = '1000000000000'; /** Separate consent domain: existing v2/v3 mock signatures never authorize custodial debits. */ export const custodyActionSchema = z.discriminatedUnion('type', [ z.object({ type: z.literal('activity'), offset: z.number().int().min(0).max(10000).default(0) }).strict(), z.object({ type: z.literal('mock-credit') }).strict(), z.object({ type: z.literal('list'), cardHash: hex, sequence: z.number().int().nonnegative().safe(), priceAtomicXmr: amount, feeBps: z.number().int().min(0).max(5000), hostFeeAddress: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{95}$/).optional() }).strict(), z.object({ type: z.literal('cancel-listing'), listingId: id }).strict(), z.object({ type: z.literal('reserve'), listingId: id, priceAtomicXmr: amount, feeBps: z.number().int().min(0).max(5000), hostFeeAddress: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{95}$/).optional() }).strict(), z.object({ type: z.literal('cancel-reservation'), orderId: id }).strict(), // No real Monero address accepted or generated in this phase. z.object({ type: z.literal('withdraw'), destination: z.literal('mock-external'), amountAtomicXmr: amount, maxFeeAtomicXmr: nonnegativeAmount }).strict(), z.object({ type: z.literal('cancel-withdrawal'), withdrawalId: id }).strict(), ]); export type CustodyAction = z.infer; export const custodyCommandSchema = z.object({ version: z.literal(1), mode: z.literal('sandbox'), policy: z.literal(CUSTODY_POLICY), network: z.literal(NETWORK), chainId: hex, custodianId: id, actor: hex, publicKey: z.string().max(256), nonce: id, expiresAt: z.number().int().nonnegative().safe(), action: custodyActionSchema, signature: z.string().max(128) }).strict(); export type CustodyCommand = z.infer; export function custodyMessage(c: Omit): string { const a = c.action; const terms = a.type === 'list' ? [a.cardHash, a.sequence, 'XMR', a.priceAtomicXmr, a.feeBps] : a.type === 'reserve' ? [a.listingId, 'XMR', a.priceAtomicXmr, a.feeBps] : a.type === 'cancel-listing' ? [a.listingId] : a.type === 'cancel-reservation' ? [a.orderId] : a.type === 'withdraw' ? [a.destination, 'XMR', a.amountAtomicXmr, a.maxFeeAtomicXmr] : a.type === 'cancel-withdrawal' ? [a.withdrawalId] : a.type === 'activity' ? [a.offset] : [MOCK_CREDIT_ATOMIC]; const fees: (string | number)[] = [ADDITIONAL_MARKET_FEE.bps, ADDITIONAL_MARKET_FEE.address]; if ((a.type === 'list' || a.type === 'reserve') && a.hostFeeAddress !== undefined) fees.push('host-fee-recipient-v1', a.hostFeeAddress); return JSON.stringify(['wikideck-custody-command-v1', c.version, c.mode, c.policy, c.network, c.chainId, c.custodianId, c.actor, c.publicKey, c.nonce, c.expiresAt, a.type, terms, fees]); } export type CustodyInfo = { enabled: true; mode: 'sandbox'; policy: typeof CUSTODY_POLICY; network: typeof NETWORK; chainId: string; custodianId: string; feeBps: number; liveDeposits: false; liveWithdrawals: false; nftSettlement: false; marketEnabled?: boolean; tradingEnabled?: boolean; hostFeeAddress?: string }; export type CustodyListing = { id: string; seller: string; cardHash: string; sequence: number; title: string; priceAtomicXmr: string; feeBps: number; status: 'open' | 'reserved' | 'cancelled' | 'sold'; createdAt: number; hostFeeAddress?: string }; export type CustodyOrder = { id: string; listingId: string; seller: string; buyer: string; cardHash: string; sequence: number; grossAtomicXmr: string; sellerAtomicXmr: string; authorAtomicXmr: string; hostAtomicXmr: string; authorAddress: string; status: 'awaiting-lock' | 'cancelled' | 'settled'; createdAt: number; hostFeeAddress?: string }; export type CustodyWithdrawal = { id: string; actor: string; destination: 'mock-external'; amountAtomicXmr: string; maxFeeAtomicXmr: string; status: 'reserved' | 'prepared' | 'complete' | 'cancelled'; attemptId?: string; transactionId?: string; feeAtomicXmr?: string; createdAt: number }; export type CustodyActivity = { availableAtomicXmr: string; purchaseReservedAtomicXmr: string; withdrawalReservedAtomicXmr: string; mockCreditClaimed: boolean; orders: CustodyOrder[]; withdrawals: CustodyWithdrawal[]; next: number | null }; export type CustodyPage = { info: CustodyInfo; listings: CustodyListing[]; next: number | null };