Paid-sale workflow
This commit is contained in:
Executable
+84
@@ -0,0 +1,84 @@
|
||||
import { z } from 'zod';
|
||||
import { ADDITIONAL_MARKET_FEE } from '../../shared/market.ts';
|
||||
import type { CustodyOrder } from '../../shared/custody.ts';
|
||||
import { saleTransactionSchema, saleTermsMessage } from '../../shared/sale-candidate.ts';
|
||||
import { applySaleBlock, initialSaleState, saleContext, saleDecision, saleId, saleStateRoot,
|
||||
termsForCustodyOrder, type SaleContext } from '../testnet/sale-candidate.ts';
|
||||
import { sha256, verifySignature } from '../testnet/consensus.ts';
|
||||
|
||||
const height = z.number().int().min(0).max(1000000);
|
||||
export const rehearsalSchema = z.object({
|
||||
policy: z.object({ coinbaseMaturity: height.positive(), lockDepth: height.positive(), outcomeDepth: height.positive(),
|
||||
maxLockBlocks: height.positive(), maxTransactions: z.number().int().min(1).max(64) }).strict(),
|
||||
seed: z.object({ height, mintedHeight: height, pageId: height.positive() }).strict(),
|
||||
lock: saleTransactionSchema.options[0],
|
||||
blocks: z.array(z.array(saleTransactionSchema).max(64)).min(1).max(128),
|
||||
}).strict();
|
||||
export type SaleRehearsal = z.infer<typeof rehearsalSchema>;
|
||||
|
||||
// This is an internal JSON-only seam, but still bound traversal before Zod or serialization.
|
||||
function checkEvidenceBudget(raw: unknown): void {
|
||||
let nodes = 0, bytes = 0;
|
||||
const seen = new WeakSet<object>();
|
||||
const visit = (value: unknown, depth: number): void => {
|
||||
if (++nodes > 32768 || depth > 12) throw new Error('Rehearsal evidence exceeds complexity budget');
|
||||
if (typeof value === 'string') {
|
||||
if (value.length > 256 * 1024) throw new Error('Rehearsal evidence exceeds budget');
|
||||
bytes += Buffer.byteLength(value) + 2;
|
||||
} else if (value !== null && typeof value === 'object') {
|
||||
if (seen.has(value)) throw new Error('Rehearsal evidence must be acyclic JSON');
|
||||
seen.add(value);
|
||||
if (!Array.isArray(value) && ![Object.prototype, null].includes(Object.getPrototypeOf(value))) throw new Error('Rehearsal evidence must be JSON');
|
||||
const keys = Object.keys(value);
|
||||
if (keys.length > (Array.isArray(value) ? 128 : 64)) throw new Error('Rehearsal evidence exceeds collection budget');
|
||||
for (const key of keys) {
|
||||
bytes += Buffer.byteLength(key) + 4;
|
||||
const property = Object.getOwnPropertyDescriptor(value, key)!;
|
||||
if (!('value' in property)) throw new Error('Rehearsal evidence must not contain accessors');
|
||||
visit(property.value, depth + 1);
|
||||
}
|
||||
seen.delete(value);
|
||||
} else if (value !== undefined && value !== null && typeof value !== 'boolean' &&
|
||||
!(typeof value === 'number' && Number.isFinite(value))) throw new Error('Rehearsal evidence must be JSON');
|
||||
if (bytes > 256 * 1024) throw new Error('Rehearsal evidence exceeds budget');
|
||||
};
|
||||
visit(raw, 0);
|
||||
}
|
||||
|
||||
/** Replays signed synthetic blocks from a caller-supplied seed, NOT a canonical-chain verifier.
|
||||
* No RPC/PoW, Monero, clock, signing key or HTTP route. Limited to one actual sandbox reservation.
|
||||
*/
|
||||
export function replayReservedSale(order: CustodyOrder, custodianId: string, raw: unknown, listingFeeBps: number) {
|
||||
checkEvidenceBudget(raw);
|
||||
const input = rehearsalSchema.parse(raw), context: SaleContext = saleContext(input.policy);
|
||||
if (Buffer.byteLength(JSON.stringify(input)) > 256 * 1024) throw new Error('Rehearsal evidence exceeds budget');
|
||||
if (input.lock.type !== 'sale-lock') throw new Error('Rehearsal requires signed lock authorization');
|
||||
const terms = input.lock.terms;
|
||||
if (terms.feeBps !== listingFeeBps) throw new Error('Rehearsal fee rate does not match reserved listing');
|
||||
const expected = termsForCustodyOrder({ ...order, status: 'awaiting-lock' }, context, { custodianId,
|
||||
host: terms.host, feeBps: terms.feeBps, lockUntilHeight: terms.lockUntilHeight, expiresHeight: terms.expiresHeight });
|
||||
if (saleTermsMessage(expected) !== saleTermsMessage(terms)) throw new Error('Rehearsal does not match reserved order terms');
|
||||
if (input.seed.mintedHeight > input.seed.height) throw new Error('Invalid rehearsal issuance height');
|
||||
const message = saleTermsMessage(terms);
|
||||
if (!verifySignature(input.lock.sellerAuthorization.publicKey, order.seller, message, input.lock.sellerAuthorization.signature) ||
|
||||
!verifySignature(input.lock.buyerAuthorization.publicKey, order.buyer, message, input.lock.buyerAuthorization.signature)) {
|
||||
throw new Error('Rehearsal requires trader consent');
|
||||
}
|
||||
const binding = sha256(JSON.stringify(['custody-sale-rehearsal-v1', custodianId, message, input.seed, input.policy]));
|
||||
const reservationDigest = sha256(JSON.stringify(['custody-reservation-rehearsal-v1', custodianId, order.id,
|
||||
order.buyer, order.seller, order.cardHash, order.sequence, order.grossAtomicXmr,
|
||||
order.sellerAtomicXmr, order.authorAtomicXmr, order.hostAtomicXmr, order.authorAddress,
|
||||
order.hostFeeAddress ?? ADDITIONAL_MARKET_FEE.address, binding]));
|
||||
let state = initialSaleState({ [order.cardHash]: { owner: order.seller, sequence: order.sequence,
|
||||
mintedHeight: input.seed.mintedHeight, pageId: input.seed.pageId } }, input.seed.height);
|
||||
for (const block of input.blocks) {
|
||||
for (const tx of block) {
|
||||
if (tx.type === 'sale-lock' ? saleId(tx.terms) !== saleId(terms) : tx.saleId !== saleId(terms)) throw new Error('Unrelated sale in rehearsal');
|
||||
if (tx.type === 'sale-complete' && tx.reservationDigest !== reservationDigest) throw new Error('Completion reservation commitment mismatch');
|
||||
}
|
||||
state = applySaleBlock(state, block, context);
|
||||
}
|
||||
const previous = order.status === 'settled' ? 'settled' : order.status === 'cancelled' ? 'refunded' : 'reserved';
|
||||
return { input, binding, reservationDigest, stateRoot: saleStateRoot(state, context),
|
||||
decision: saleDecision(state, terms, context, previous), context };
|
||||
}
|
||||
Reference in New Issue
Block a user