Planning
This commit is contained in:
Executable
+67
@@ -0,0 +1,67 @@
|
||||
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 }).strict(),
|
||||
z.object({ type: z.literal('bid'), listingId: id, priceAtomicXmr: atomicPrice }).strict(),
|
||||
z.object({ type: z.literal('buy'), listingId: id, priceAtomicXmr: atomicPrice, feeBps }).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<typeof marketActionV3Schema>;
|
||||
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<typeof marketCommandV3Schema>;
|
||||
|
||||
/** New domain: price and currency are exact XMR atomic units, never fiat-converted. */
|
||||
export function marketMessageV3(command: Omit<MarketCommandV3, 'signature'>): 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 = a.type === 'list' || a.type === 'buy' ? [ADDITIONAL_MARKET_FEE.bps, ADDITIONAL_MARKET_FEE.address] : [];
|
||||
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<MarketListing, 'priceCents' | 'highestBid'> & {
|
||||
termsVersion: 3; priceAtomicXmr: string;
|
||||
highestBid: { bidder: string; priceAtomicXmr: string } | null;
|
||||
};
|
||||
export type MarketOrderV3 = Omit<MarketOrder, 'priceCents'> & { 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;
|
||||
}
|
||||
+2
-1
@@ -60,7 +60,8 @@ export type MarketOrder = {
|
||||
additionalFee?: AdditionalFeePolicy & { amountXmr: string; payout: 'not-started' | 'mock-paid' | 'review' };
|
||||
};
|
||||
export type MarketInfo = { network: string; chainId: string; marketId: string; paymentMode: 'mock';
|
||||
feeBps: number; additionalFee: AdditionalFeePolicy; deliveryConfirmations: number; mockUsdPerXmr: string; serverTime: number };
|
||||
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);
|
||||
|
||||
@@ -52,4 +52,4 @@ export function xmrCommission(gross: bigint, bps: number): bigint {
|
||||
assertXmrAmount(gross);
|
||||
if (!Number.isSafeInteger(bps) || bps < 0 || bps > 10_000) throw new Error('Invalid commission basis points');
|
||||
return gross * BigInt(bps) / 10_000n;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,4 +27,4 @@ export function assessXmrSaleBudget(gross: bigint, hostingBps: number, sellerNet
|
||||
sellerMinimumNet: viable ? available - sellerNetworkBudget : 0n,
|
||||
// Additional gross-independent funding required to retain at least one atomic unit.
|
||||
fundingShortfall: viable ? 0n : sellerNetworkBudget - available + 1n };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user