Markets
This commit is contained in:
Executable
+72
@@ -0,0 +1,72 @@
|
||||
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<typeof marketActionSchema>;
|
||||
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<typeof marketCommandSchema>;
|
||||
export function marketMessage(command: Omit<MarketCommand, 'signature'>): 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';
|
||||
feeBps: number; additionalFee: AdditionalFeePolicy; deliveryConfirmations: number; mockUsdPerXmr: string; 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')));
|
||||
}
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
/** Selection is fixed at server startup / browser build, never learned from an untrusted RPC. */
|
||||
export function networkProfile(value: string | undefined): 'testnet' | 'mainnet' {
|
||||
const profile = value ?? 'testnet';
|
||||
if (profile !== 'testnet' && profile !== 'mainnet') throw new Error('WIKIDECK_NETWORK must be testnet or mainnet');
|
||||
return profile;
|
||||
}
|
||||
export const PROFILE = networkProfile(typeof process !== 'undefined' && process.versions?.node
|
||||
? process.env.WIKIDECK_NETWORK : import.meta.env?.VITE_WIKIDECK_NETWORK);
|
||||
export const IS_MAINNET = PROFILE === 'mainnet';
|
||||
export const NETWORK = IS_MAINNET ? 'wikideck-randomx-mainnet-1' as const : 'wikideck-randomx-testnet-6' as const;
|
||||
// Candidate mainnet genesis: 2027-01-01 00:00 UTC. NOT a commitment to a launch date.
|
||||
export const NETWORK_GENESIS_TIME = IS_MAINNET ? 1_798_761_600 : 1_792_411_200;
|
||||
export const NETWORK_LABEL = IS_MAINNET ? 'MAINNET CANDIDATE' : 'TESTNET';
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
import type { Article } from './protocol.ts';
|
||||
import type { MintStatsSnapshot } from './enrichment.ts';
|
||||
|
||||
export const NETWORK = 'wikideck-randomx-testnet-6' as const;
|
||||
import { NETWORK } from './network.ts';
|
||||
export { NETWORK } from './network.ts';
|
||||
export const CONSENSUS = {
|
||||
version: 6, targetSeconds: 15 * 60, retargetInterval: 10, medianWindow: 11,
|
||||
commitments: 'sha256-domain-separated-counted-merkle-v1', randomx: '1.2.1', powEndian: 'big',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Browser/Node WebCrypto only: this module never reads storage or sends data.
|
||||
const NETWORK = 'wikideck-randomx-testnet-6';
|
||||
import { NETWORK } from './network.ts';
|
||||
const ITERATIONS = 600_000;
|
||||
const MAX_BYTES = 32 * 1024;
|
||||
const encoder = new TextEncoder();
|
||||
@@ -7,7 +7,7 @@ const encoder = new TextEncoder();
|
||||
export type WalletBackup = {
|
||||
format: 'wikideck-wallet-backup';
|
||||
version: 1;
|
||||
network: 'wikideck-randomx-testnet-6';
|
||||
network: typeof NETWORK;
|
||||
kdf: 'PBKDF2-SHA256';
|
||||
iterations: 600000;
|
||||
cipher: 'AES-256-GCM';
|
||||
|
||||
Reference in New Issue
Block a user