This commit is contained in:
2026-10-20 03:14:08 -04:00
parent b1d2b5c10e
commit 9850fd8554
28 changed files with 1256 additions and 26 deletions
+72
View File
@@ -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')));
}
+13
View File
@@ -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
View File
@@ -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',
+2 -2
View File
@@ -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';