import { z } from 'zod'; import { NETWORK, type NftTransfer } from './testnet.ts'; import { ADDITIONAL_MARKET_FEE, type MarketInfo, type MarketListing, type MarketOrder } from './market.ts'; import { MAX_XMR_AMOUNT } from './xmr-amount.ts'; const id = z.string().uuid(); const hex = z.string().regex(/^[a-f0-9]{64}$/); const atomicPrice = z.string().regex(/^(0|[1-9]\d{0,21})$/).refine(value => { try { const amount = BigInt(value); return amount > 0n && amount <= MAX_XMR_AMOUNT; } catch { return false; } }, 'XMR price must be a positive bounded atomic amount'); const feeBps = z.number().int().min(0).max(5000); export const marketActionV3Schema = z.discriminatedUnion('type', [ z.object({ type: z.literal('list'), cardHash: hex, sequence: z.number().int().nonnegative().safe(), kind: z.enum(['fixed', 'auction']), priceAtomicXmr: atomicPrice, endsAt: z.number().int().nonnegative().nullable(), feeBps, hostFeeAddress: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{95}$/).optional() }).strict(), z.object({ type: z.literal('bid'), listingId: id, priceAtomicXmr: atomicPrice }).strict(), z.object({ type: z.literal('buy'), listingId: id, priceAtomicXmr: atomicPrice, feeBps, hostFeeAddress: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{95}$/).optional() }).strict(), z.object({ type: z.literal('cancel'), listingId: id }).strict(), z.object({ type: z.literal('retire-legacy-listing'), listingId: id }).strict(), z.object({ type: z.literal('mock-pay'), orderId: id }).strict(), z.object({ type: z.literal('refund'), orderId: id }).strict(), z.object({ type: z.literal('deliver'), orderId: id, transfer: z.object({ network: z.literal(NETWORK), chainId: hex, cardHash: hex, from: hex, to: hex, sequence: z.number().int().nonnegative().safe(), publicKey: z.string().max(256), signature: z.string().max(128) }).strict() }).strict(), z.object({ type: z.literal('activity') }).strict(), ]); export type MarketActionV3 = z.infer; export const marketCommandV3Schema = z.object({ version: z.literal(3), network: z.literal(NETWORK), chainId: hex, marketId: id, actor: hex, publicKey: z.string().max(256), nonce: id, expiresAt: z.number().int().nonnegative().safe(), action: marketActionV3Schema, signature: z.string().max(128) }).strict(); export type MarketCommandV3 = z.infer; /** New domain: price and currency are exact XMR atomic units, never fiat-converted. */ export function marketMessageV3(command: Omit): string { const a = command.action; const terms = a.type === 'list' ? [a.cardHash, a.sequence, a.kind, 'XMR', a.priceAtomicXmr, a.endsAt, a.feeBps] : a.type === 'bid' ? [a.listingId, 'XMR', a.priceAtomicXmr] : a.type === 'buy' ? [a.listingId, 'XMR', a.priceAtomicXmr, a.feeBps] : a.type === 'cancel' || a.type === 'retire-legacy-listing' ? [a.listingId] : a.type === 'activity' ? [] : a.type === 'deliver' ? [a.orderId, a.transfer.network, a.transfer.chainId, a.transfer.cardHash, a.transfer.from, a.transfer.to, a.transfer.sequence, a.transfer.publicKey, a.transfer.signature] : [a.orderId]; const feeTerms: (string | number)[] = a.type === 'list' || a.type === 'buy' ? [ADDITIONAL_MARKET_FEE.bps, ADDITIONAL_MARKET_FEE.address] : []; // Preserve original v3 message bytes if omitted; omission authorizes ONLY the default author recipient. if ((a.type === 'list' || a.type === 'buy') && a.hostFeeAddress !== undefined) feeTerms.push('host-fee-recipient-v1', a.hostFeeAddress); return JSON.stringify([NETWORK, 'market-command-v3', command.version, command.chainId, command.marketId, command.actor, command.publicKey, command.nonce, command.expiresAt, a.type, terms, feeTerms]); } export type MarketListingV3 = Omit & { termsVersion: 3; priceAtomicXmr: string; highestBid: { bidder: string; priceAtomicXmr: string } | null; }; export type MarketOrderV3 = Omit & { termsVersion: 3; priceAtomicXmr: string }; export type MarketListingRecord = MarketListing | MarketListingV3; export type MarketOrderRecord = MarketOrder | MarketOrderV3; export type MarketInfoV3 = MarketInfo & { marketVersion: 3 }; export type MarketActivityV3 = { listings: MarketListingRecord[]; orders: MarketOrderRecord[] }; export function isXmrListing(listing: MarketListingRecord): listing is MarketListingV3 { return 'priceAtomicXmr' in listing; } export function isXmrOrder(order: MarketOrderRecord): order is MarketOrderV3 { return 'priceAtomicXmr' in order; }