import { z } from 'zod'; import { NETWORK, type NftTransfer } from './testnet.ts'; export const usdCents = z.number().int().min(1).max(100_000_000); /** Public payout destination, not a secret or a buyer invoice address. Not environment-configurable. */ export const ADDITIONAL_MARKET_FEE = Object.freeze({ bps: 100, address: '44xHcCdFhW4b951yBHpkTPdLSJ85qTCnFQ8N7m96TtqqRL3dbhzbEH4fe2SJ3a6ESHcNyzCYH5nYJ9ActR1JtkeA7QYGbQv' }); export type AdditionalFeePolicy = { bps: number; address: string }; const id = z.string().uuid(); const hex = z.string().regex(/^[a-f0-9]{64}$/); export const marketActionSchema = z.discriminatedUnion('type', [ z.object({ type: z.literal('list'), cardHash: hex, sequence: z.number().int().nonnegative(), kind: z.enum(['fixed', 'auction']), priceCents: usdCents, endsAt: z.number().int().nonnegative().nullable(), feeBps: z.number().int().min(0).max(5000) }).strict(), z.object({ type: z.literal('bid'), listingId: id, priceCents: usdCents }).strict(), z.object({ type: z.literal('buy'), listingId: id, priceCents: usdCents, feeBps: z.number().int().min(0).max(5000) }).strict(), z.object({ type: z.literal('cancel'), listingId: id }).strict(), z.object({ type: z.literal('mock-pay'), orderId: id }).strict(), z.object({ type: z.literal('refund'), orderId: id }).strict(), // Delivery is an ordinary chain-validated transfer, not a market signature substitute. 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(), publicKey: z.string().max(256), signature: z.string().max(128) }).strict() }).strict(), z.object({ type: z.literal('activity') }).strict(), ]); export type MarketAction = z.infer; export const marketCommandSchema = z.object({ network: z.literal(NETWORK), chainId: hex, marketId: id, actor: hex, publicKey: z.string().max(256), nonce: id, expiresAt: z.number().int().nonnegative(), action: marketActionSchema, signature: z.string().max(128) }).strict(); export type MarketCommand = z.infer; export function marketMessage(command: Omit): string { const a = command.action; const terms = a.type === 'list' ? [a.cardHash, a.sequence, a.kind, a.priceCents, a.endsAt, a.feeBps] : a.type === 'bid' ? [a.listingId, a.priceCents] : a.type === 'buy' ? [a.listingId, a.priceCents, a.feeBps] : a.type === 'cancel' ? [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]; // Bind the non-configurable fee and recipient to sale authorization, not just UI disclosure. const feeTerms = a.type === 'list' || a.type === 'buy' ? [ADDITIONAL_MARKET_FEE.bps, ADDITIONAL_MARKET_FEE.address] : []; return JSON.stringify([NETWORK, 'market-command-v2', command.chainId, command.marketId, command.actor, command.publicKey, command.nonce, command.expiresAt, a.type, terms, feeTerms]); } export type MarketListing = { id: string; cardHash: string; sequence: number; seller: string; title: string; image: string; kind: 'fixed' | 'auction'; priceCents: number; feeBps: number; createdAt: number; endsAt: number | null; status: 'open' | 'reserved' | 'sold' | 'cancelled' | 'expired' | 'invalid'; highestBid: { bidder: string; priceCents: number } | null; additionalFee?: AdditionalFeePolicy; // Absent on legacy listings; relisting requires fresh fee authorization. }; export type MarketOrder = { id: string; listingId: string; seller: string; buyer: string; cardHash: string; sequence: number; priceCents: number; feeBps: number; createdAt: number; payBy: number; deliverBy: number; status: 'awaiting-payment' | 'paid' | 'delivery-pending' | 'complete' | 'expired' | 'refund-required' | 'refunded' | 'review'; invoice: { provider: 'mock'; amountXmr: string; address: null; label: string }; feeXmr: string; sellerNetXmr: string; payout: 'not-started' | 'mock-paid' | 'review'; transfer?: NftTransfer; transferId?: string; deliveryConfirmations: number; requiredConfirmations: number; additionalFee?: AdditionalFeePolicy & { amountXmr: string; payout: 'not-started' | 'mock-paid' | 'review' }; }; export type MarketInfo = { network: string; chainId: string; marketId: string; paymentMode: 'mock'; marketVersion: 3; feeBps: number; additionalFee: AdditionalFeePolicy; deliveryConfirmations: number; serverTime: number }; export type MarketActivity = { orders: MarketOrder[]; listings: MarketListing[] }; export function formatUsd(cents: number): string { return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(cents / 100); } export function parseUsd(text: string): number { if (!/^(0|[1-9]\d{0,6})(\.\d{1,2})?$/.test(text)) throw new Error('Enter a USD amount with at most two decimal places.'); const [whole, fraction = ''] = text.split('.'); return usdCents.parse(Number(whole) * 100 + Number(fraction.padEnd(2, '0'))); }