Remove markets

This commit is contained in:
2026-10-28 18:33:52 -04:00
parent eea2deb1fb
commit b3a1179703
141 changed files with 554 additions and 17365 deletions
-69
View File
@@ -1,69 +0,0 @@
import { z } from 'zod';
import { NETWORK } from './testnet.ts';
import { ADDITIONAL_MARKET_FEE } from './market.ts';
import { parseAtomicXmr } from './xmr-amount.ts';
const hex = z.string().regex(/^[a-f0-9]{64}$/);
const id = z.string().uuid();
const amount = z.string().max(22).refine(value => {
try { return parseAtomicXmr(value) > 0n; } catch { return false; }
}, 'Expected a positive canonical atomic XMR amount');
const nonnegativeAmount = z.string().max(22).refine(value => {
try { parseAtomicXmr(value); return true; } catch { return false; }
}, 'Expected canonical atomic XMR amount');
export const CUSTODY_POLICY = 'custody-sandbox-v1';
export const MOCK_CREDIT_ATOMIC = '1000000000000';
/** Separate consent domain: existing v2/v3 mock signatures never authorize custodial debits. */
export const custodyActionSchema = z.discriminatedUnion('type', [
z.object({ type: z.literal('activity'), offset: z.number().int().min(0).max(10000).default(0) }).strict(),
z.object({ type: z.literal('mock-credit') }).strict(),
z.object({ type: z.literal('list'), cardHash: hex, sequence: z.number().int().nonnegative().safe(),
priceAtomicXmr: amount, feeBps: z.number().int().min(0).max(5000),
hostFeeAddress: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{95}$/).optional() }).strict(),
z.object({ type: z.literal('cancel-listing'), listingId: id }).strict(),
z.object({ type: z.literal('reserve'), listingId: id, priceAtomicXmr: amount,
feeBps: z.number().int().min(0).max(5000), hostFeeAddress: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{95}$/).optional() }).strict(),
z.object({ type: z.literal('cancel-reservation'), orderId: id }).strict(),
// No real Monero address accepted or generated in this phase.
z.object({ type: z.literal('withdraw'), destination: z.literal('mock-external'), amountAtomicXmr: amount,
maxFeeAtomicXmr: nonnegativeAmount }).strict(),
z.object({ type: z.literal('cancel-withdrawal'), withdrawalId: id }).strict(),
]);
export type CustodyAction = z.infer<typeof custodyActionSchema>;
export const custodyCommandSchema = z.object({ version: z.literal(1), mode: z.literal('sandbox'),
policy: z.literal(CUSTODY_POLICY), network: z.literal(NETWORK), chainId: hex, custodianId: id,
actor: hex, publicKey: z.string().max(256), nonce: id, expiresAt: z.number().int().nonnegative().safe(),
action: custodyActionSchema, signature: z.string().max(128) }).strict();
export type CustodyCommand = z.infer<typeof custodyCommandSchema>;
export function custodyMessage(c: Omit<CustodyCommand, 'signature'>): string {
const a = c.action;
const terms = a.type === 'list' ? [a.cardHash, a.sequence, 'XMR', a.priceAtomicXmr, a.feeBps]
: a.type === 'reserve' ? [a.listingId, 'XMR', a.priceAtomicXmr, a.feeBps]
: a.type === 'cancel-listing' ? [a.listingId]
: a.type === 'cancel-reservation' ? [a.orderId]
: a.type === 'withdraw' ? [a.destination, 'XMR', a.amountAtomicXmr, a.maxFeeAtomicXmr]
: a.type === 'cancel-withdrawal' ? [a.withdrawalId]
: a.type === 'activity' ? [a.offset] : [MOCK_CREDIT_ATOMIC];
const fees: (string | number)[] = [ADDITIONAL_MARKET_FEE.bps, ADDITIONAL_MARKET_FEE.address];
if ((a.type === 'list' || a.type === 'reserve') && a.hostFeeAddress !== undefined) fees.push('host-fee-recipient-v1', a.hostFeeAddress);
return JSON.stringify(['wikideck-custody-command-v1', c.version, c.mode, c.policy, c.network,
c.chainId, c.custodianId, c.actor, c.publicKey, c.nonce, c.expiresAt, a.type, terms,
fees]);
}
export type CustodyInfo = { enabled: true; mode: 'sandbox'; policy: typeof CUSTODY_POLICY;
network: typeof NETWORK; chainId: string; custodianId: string; feeBps: number;
liveDeposits: false; liveWithdrawals: false; nftSettlement: false;
marketEnabled?: boolean; tradingEnabled?: boolean; hostFeeAddress?: string };
export type CustodyListing = { id: string; seller: string; cardHash: string; sequence: number;
title: string; priceAtomicXmr: string; feeBps: number; status: 'open' | 'reserved' | 'cancelled' | 'sold'; createdAt: number; hostFeeAddress?: string };
export type CustodyOrder = { id: string; listingId: string; seller: string; buyer: string; cardHash: string;
sequence: number; grossAtomicXmr: string; sellerAtomicXmr: string; authorAtomicXmr: string;
hostAtomicXmr: string; authorAddress: string; status: 'awaiting-lock' | 'cancelled' | 'settled'; createdAt: number; hostFeeAddress?: string };
export type CustodyWithdrawal = { id: string; actor: string; destination: 'mock-external';
amountAtomicXmr: string; maxFeeAtomicXmr: string; status: 'reserved' | 'prepared' | 'complete' | 'cancelled';
attemptId?: string; transactionId?: string; feeAtomicXmr?: string; createdAt: number };
export type CustodyActivity = { availableAtomicXmr: string; purchaseReservedAtomicXmr: string;
withdrawalReservedAtomicXmr: string; mockCreditClaimed: boolean; orders: CustodyOrder[];
withdrawals: CustodyWithdrawal[]; next: number | null };
export type CustodyPage = { info: CustodyInfo; listings: CustodyListing[]; next: number | null };
-71
View File
@@ -1,71 +0,0 @@
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<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: (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<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;
}
-75
View File
@@ -1,75 +0,0 @@
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.
hostFeeAddress?: string; // Snapshotted signed recipient on new records; never rewrite historical terms.
};
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' };
hostFeeAddress?: string;
};
export type MarketInfo = { network: string; chainId: string; marketId: string; paymentMode: 'mock';
marketVersion: 3; feeBps: number; additionalFee: AdditionalFeePolicy; deliveryConfirmations: number;
serverTime: number; marketEnabled?: boolean; tradingEnabled?: boolean; hostFeeAddress?: string };
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')));
}
-33
View File
@@ -1,33 +0,0 @@
import { z } from 'zod';
const hex64 = z.string().regex(/^[a-f0-9]{64}$/);
const uuid = z.string().uuid();
const network = z.enum(['wikideck-randomx-mainnet-1', 'wikideck-randomx-testnet-6']);
/** Wallet-signed request to allocate the caller's own immutable Monero subaddress. */
export const moneroDepositAddressRequestSchema = z.object({
version: z.literal(1),
network,
chainId: hex64,
custodianId: uuid,
actor: hex64,
publicKey: z.string().min(1).max(256),
requestId: uuid,
walletGenerationId: uuid,
walletFingerprint: hex64,
accountIndex: z.number().int().min(0).max(0xffff_ffff).safe(),
expiresAt: z.number().int().nonnegative().safe(),
signature: z.string().min(1).max(256),
}).strict();
export type MoneroDepositAddressRequest = z.infer<typeof moneroDepositAddressRequestSchema>;
export type MoneroDepositAddressRequestUnsigned = Omit<MoneroDepositAddressRequest, 'signature'>;
/** Domain-separated, canonical terms for requesting one beneficiary-bound deposit address. */
export function moneroDepositAddressMessage(request: MoneroDepositAddressRequestUnsigned): string {
return JSON.stringify([
'wikideck-monero-deposit-address-v1', request.version, request.network, request.chainId,
request.custodianId, request.actor, request.publicKey, request.requestId,
request.walletGenerationId, request.walletFingerprint, request.accountIndex, request.expiresAt,
]);
}
-70
View File
@@ -1,70 +0,0 @@
import { z } from 'zod';
import { parseAtomicXmr } from './xmr-amount.ts';
const uuid = z.string().uuid();
const hex = z.string().regex(/^[a-f0-9]{64}$/);
const atomic = z.string().max(22).refine(value => { try { parseAtomicXmr(value); return true; } catch { return false; } });
const positiveAtomic = atomic.refine(value => BigInt(value) > 0n);
export const mainnetWithdrawalRequestSchema = z.object({
version: z.literal(1),
network: z.literal('wikideck-randomx-mainnet-1'),
chainId: hex,
custodianId: uuid,
beneficiary: hex,
publicKey: z.string().min(1).max(256),
requestId: uuid,
destination: z.string().length(95),
amountAtomicXmr: positiveAtomic,
maxFeeAtomicXmr: atomic,
expiresAt: z.number().int().nonnegative().safe(),
signature: z.string().min(1).max(256),
}).strict();
export type MainnetWithdrawalRequest = z.infer<typeof mainnetWithdrawalRequestSchema>;
export type MainnetWithdrawalRequestUnsigned = Omit<MainnetWithdrawalRequest, 'signature'>;
export const mainnetWithdrawalCancellationSchema = z.object({
version: z.literal(1),
network: z.literal('wikideck-randomx-mainnet-1'),
chainId: hex,
custodianId: uuid,
beneficiary: hex,
publicKey: z.string().min(1).max(256),
cancellationId: uuid,
withdrawalRequestId: uuid,
expiresAt: z.number().int().nonnegative().safe(),
signature: z.string().min(1).max(256),
}).strict();
export type MainnetWithdrawalCancellation = z.infer<typeof mainnetWithdrawalCancellationSchema>;
export type MainnetWithdrawalCancellationUnsigned = Omit<MainnetWithdrawalCancellation, 'signature'>;
export function mainnetWithdrawalMessage(request: MainnetWithdrawalRequestUnsigned): string {
return JSON.stringify(['wikideck-mainnet-withdrawal-request-v1', request.version, request.network,
request.chainId, request.custodianId, request.beneficiary, request.publicKey, request.requestId,
request.destination, request.amountAtomicXmr, request.maxFeeAtomicXmr, request.expiresAt]);
}
export function mainnetWithdrawalCancellationMessage(request: MainnetWithdrawalCancellationUnsigned): string {
return JSON.stringify(['wikideck-mainnet-withdrawal-cancel-v1', request.version, request.network,
request.chainId, request.custodianId, request.beneficiary, request.publicKey, request.cancellationId,
request.withdrawalRequestId, request.expiresAt]);
}
export const mainnetCustodyBalanceRequestSchema = z.object({
version: z.literal(1),
network: z.literal('wikideck-randomx-mainnet-1'),
chainId: hex,
custodianId: uuid,
beneficiary: hex,
publicKey: z.string().min(1).max(256),
nonce: uuid,
expiresAt: z.number().int().nonnegative().safe(),
signature: z.string().min(1).max(256),
}).strict();
export type MainnetCustodyBalanceRequest = z.infer<typeof mainnetCustodyBalanceRequestSchema>;
export type MainnetCustodyBalanceRequestUnsigned = Omit<MainnetCustodyBalanceRequest, 'signature'>;
export function mainnetCustodyBalanceMessage(request: MainnetCustodyBalanceRequestUnsigned): string {
return JSON.stringify(['wikideck-mainnet-custody-balance-v1', request.version, request.network,
request.chainId, request.custodianId, request.beneficiary, request.publicKey, request.nonce, request.expiresAt]);
}
+6 -8
View File
@@ -1,18 +1,16 @@
/** Selection is fixed at server startup / browser build, never learned from an untrusted RPC. */
export type NetworkProfile = 'testnet' | 'mainnet' | 'pilot';
export type NetworkProfile = 'testnet' | 'mainnet';
export function networkProfile(value: string | undefined): NetworkProfile {
const profile = value ?? 'testnet';
if (profile !== 'testnet' && profile !== 'mainnet' && profile !== 'pilot') {
throw new Error('WIKIDECK_NETWORK must be testnet, mainnet, or pilot');
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 IS_PILOT = PROFILE === 'pilot';
export const NETWORK = IS_MAINNET ? 'wikideck-randomx-mainnet-1' as const
: IS_PILOT ? 'wikideck-randomx-pilot-1' as const : 'wikideck-randomx-testnet-6' as const;
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 : IS_PILOT ? 1_792_627_200 : 1_792_411_200;
export const NETWORK_LABEL = IS_MAINNET ? 'MAINNET CANDIDATE' : IS_PILOT ? 'PRIVATE PILOT' : 'TESTNET';
export const NETWORK_GENESIS_TIME = IS_MAINNET ? 1_798_761_600 : 1_792_411_200;
export const NETWORK_LABEL = IS_MAINNET ? 'MAINNET CANDIDATE' : 'TESTNET';
-34
View File
@@ -1,34 +0,0 @@
import { z } from 'zod';
const hex64 = z.string().regex(/^[a-f0-9]{64}$/);
const uuid = z.string().uuid();
/** Pilot-specific wallet authorization. Market and Monero network identities are both explicit and signed. */
export const pilotMoneroDepositRequestSchema = z.object({
version: z.literal(1),
marketNetwork: z.literal('wikideck-randomx-pilot-1'),
marketChainId: hex64,
moneroNetwork: z.literal('mainnet'),
custodianId: uuid,
beneficiary: hex64,
publicKey: z.string().min(1).max(256),
requestId: uuid,
walletGenerationId: uuid,
walletFingerprint: hex64,
accountIndex: z.number().int().min(0).max(0xffff_ffff).safe(),
expiresAt: z.number().int().nonnegative().safe(),
signature: z.string().min(1).max(256),
}).strict();
export type PilotMoneroDepositRequest = z.infer<typeof pilotMoneroDepositRequestSchema>;
export type PilotMoneroDepositRequestUnsigned = Omit<PilotMoneroDepositRequest, 'signature'>;
/** Canonical, domain-separated wallet authorization; both asset networks are signature-bound. */
export function pilotMoneroDepositRequestMessage(request: PilotMoneroDepositRequestUnsigned): string {
return JSON.stringify([
'wikideck-pilot-monero-deposit-request-v1', request.version,
request.marketNetwork, request.marketChainId, request.moneroNetwork, request.custodianId,
request.beneficiary, request.publicKey, request.requestId, request.walletGenerationId,
request.walletFingerprint, request.accountIndex, request.expiresAt,
]);
}
-44
View File
@@ -1,44 +0,0 @@
import { z } from 'zod';
import { NETWORK } from './network.ts';
import { ADDITIONAL_MARKET_FEE } from './market.ts';
import { parseAtomicXmr } from './xmr-amount.ts';
/** REVIEW CANDIDATE ONLY. Never accepted by the active transactionSchema or custody HTTP API. */
export const SALE_CANDIDATE = 'custodial-sale-lock-candidate-v1';
const hex = z.string().regex(/^[a-f0-9]{64}$/);
const height = z.number().int().nonnegative().safe();
const amount = z.string().max(22).refine(s => { try { return parseAtomicXmr(s) > 0n; } catch { return false; } });
export const saleTermsSchema = z.object({ protocol: z.literal(SALE_CANDIDATE), network: z.literal(NETWORK),
chainId: hex, policyDigest: hex, custodianId: z.string().uuid(), orderId: z.string().uuid(),
cardHash: hex, sequence: height, seller: hex, buyer: hex, host: hex,
priceAtomicXmr: amount, feeBps: z.number().int().min(0).max(5000),
hostFeeAddress: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{95}$/).optional(),
lockUntilHeight: height, expiresHeight: height }).strict();
export type SaleTerms = z.infer<typeof saleTermsSchema>;
export const saleSignatureSchema = z.object({ publicKey: z.string().min(1).max(256),
signature: z.string().regex(/^[A-Za-z0-9+/]+={0,2}$/).length(88) }).strict();
const reference = { protocol: z.literal(SALE_CANDIDATE), chainId: hex, saleId: hex };
export const saleTransactionSchema = z.discriminatedUnion('type', [
z.object({ type: z.literal('sale-lock'), terms: saleTermsSchema,
sellerAuthorization: saleSignatureSchema, buyerAuthorization: saleSignatureSchema }).strict(),
z.object({ type: z.literal('sale-complete'), ...reference, reservationDigest: hex,
hostAuthorization: saleSignatureSchema }).strict(),
z.object({ type: z.literal('sale-cancel'), ...reference,
hostAuthorization: saleSignatureSchema, buyerAuthorization: saleSignatureSchema }).strict(),
z.object({ type: z.literal('sale-expire'), ...reference }).strict(),
]);
export type SaleTransaction = z.infer<typeof saleTransactionSchema>;
export function saleTermsMessage(t: SaleTerms): string {
const tuple: unknown[] = [SALE_CANDIDATE, 'terms', t.network, t.chainId, t.policyDigest,
t.custodianId, t.orderId, t.cardHash, t.sequence, t.seller, t.buyer, t.host,
'XMR', t.priceAtomicXmr, t.feeBps, ADDITIONAL_MARKET_FEE.bps, ADDITIONAL_MARKET_FEE.address,
t.lockUntilHeight, t.expiresHeight];
if (t.hostFeeAddress !== undefined) tuple.push(['host-fee-recipient-v1', t.hostFeeAddress]);
return JSON.stringify(tuple);
}
export function saleCompletionMessage(t: Pick<Extract<SaleTransaction, { type: 'sale-complete' }>, 'chainId' | 'saleId' | 'reservationDigest'>): string {
return JSON.stringify([SALE_CANDIDATE, 'complete-funded-reservation', t.chainId, t.saleId, t.reservationDigest]);
}
export function saleCancellationMessage(t: { chainId: string; saleId: string }): string {
return JSON.stringify([SALE_CANDIDATE, 'cancel', t.chainId, t.saleId]);
}
-55
View File
@@ -1,55 +0,0 @@
/** Exact, browser-safe amounts. No wallet RPC, exchange rate, custody or signing policy. */
export const XMR_ATOMIC_UNITS = 1_000_000_000_000n;
// Preserve the existing processor decimal parser's ten-whole-digit encoding bound.
// This is NOT a supply limit, an exposure approval, or a wallet-RPC uint64 guarantee.
export const MAX_XMR_AMOUNT = 10_000_000_000n * XMR_ATOMIC_UNITS - 1n;
export const XMR_RPC_UINT64_MAX = (1n << 64n) - 1n;
export function assertXmrAmount(amount: bigint): void {
if (typeof amount !== 'bigint' || amount < 0n || amount > MAX_XMR_AMOUNT) {
throw new Error('Invalid bounded atomic XMR amount');
}
}
/** Decimal user/processor input; never coerce, trim, round, or accept floating point. */
export function parseXmr(value: string): bigint {
if (typeof value !== 'string' || value.length > 23 || !/^(0|[1-9]\d{0,9})(\.\d{1,12})?(?![\s\S])/.test(value)) {
throw new Error('Invalid bounded XMR decimal');
}
const [whole, fraction = ''] = value.split('.');
return BigInt(whole) * XMR_ATOMIC_UNITS + BigInt(fraction.padEnd(12, '0'));
}
/** Canonical display/processor decimal: always twelve fractional places. */
export function formatXmr(amount: bigint): string {
assertXmrAmount(amount);
return `${amount / XMR_ATOMIC_UNITS}.${(amount % XMR_ATOMIC_UNITS).toString().padStart(12, '0')}`;
}
/** JSON/signature/storage representation: canonical base-ten integer string, not a JSON number. */
export function parseAtomicXmr(value: string): bigint {
if (typeof value !== 'string' || value.length > 22 || !/^(0|[1-9]\d{0,21})(?![\s\S])/.test(value)) {
throw new Error('Invalid canonical atomic XMR string');
}
const amount = BigInt(value);
assertXmrAmount(amount);
return amount;
}
export function atomicXmrString(amount: bigint): string {
assertXmrAmount(amount);
return amount.toString();
}
/** RPC amounts need lossless JSON encoding too; Number(amount) is not safe. */
export function assertXmrRpcAmount(amount: bigint): void {
assertXmrAmount(amount);
if (amount > XMR_RPC_UINT64_MAX) throw new Error('XMR amount exceeds wallet-RPC uint64');
}
/** Independently round commissions down; all sub-atomic remainders stay with the seller. */
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;
}
-30
View File
@@ -1,30 +0,0 @@
import { ADDITIONAL_MARKET_FEE } from './market.ts';
import { assertXmrAmount, xmrCommission } from './xmr-amount.ts';
/** Allocation only, not proof of payment or an instruction to send/retain funds. */
export function allocateXmrSale(gross: bigint, hostingBps: number) {
assertXmrAmount(gross);
if (gross === 0n) throw new Error('Sale price must be positive; use a free transfer for gifts');
if (!Number.isSafeInteger(hostingBps) || hostingBps < 0 || hostingBps > 5000) {
throw new Error('Hosting commission must be between 0 and 5000 basis points');
}
const hosting = xmrCommission(gross, hostingBps);
const author = xmrCommission(gross, ADDITIONAL_MARKET_FEE.bps);
return { gross, hosting, author, sellerBeforeNetworkCosts: gross - hosting - author };
}
/**
* Evaluate an explicitly supplied total seller-funded network-cost budget, including
* every applicable leg. Does not choose who funds an author leg or approve a quote.
* The budget is a maximum, not revenue; actual costs/unused budget need accounting.
*/
export function assessXmrSaleBudget(gross: bigint, hostingBps: number, sellerNetworkBudget: bigint) {
const allocation = allocateXmrSale(gross, hostingBps);
assertXmrAmount(sellerNetworkBudget);
const available = allocation.sellerBeforeNetworkCosts;
const viable = sellerNetworkBudget < available;
return { ...allocation, sellerNetworkBudget, viable,
sellerMinimumNet: viable ? available - sellerNetworkBudget : 0n,
// Additional gross-independent funding required to retain at least one atomic unit.
fundingShortfall: viable ? 0n : sellerNetworkBudget - available + 1n };
}