import test from 'node:test'; import assert from 'node:assert/strict'; import { generateKeyPairSync, randomUUID, sign } from 'node:crypto'; import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { DatabaseSync } from 'node:sqlite'; import { CustodyMarket, custodyMode } from '../server/market/custody.ts'; import { CUSTODY_POLICY, MOCK_CREDIT_ATOMIC, custodyCommandSchema, custodyMessage, type CustodyAction, type CustodyActivity, type CustodyListing, type CustodyOrder, type CustodyWithdrawal, type CustodyCommand } from '../shared/custody.ts'; import { NETWORK } from '../shared/testnet.ts'; import { address, CHAIN_ID } from '../server/testnet/consensus.ts'; import { operatorMarketPolicy, validateMarketPolicy, type MarketPolicy } from '../server/market/policy.ts'; import { ADDITIONAL_MARKET_FEE } from '../shared/market.ts'; import { keccak } from 'hash-wasm'; function identity() { const keys = generateKeyPairSync('ec', { namedCurve: 'prime256v1' }); const publicKey = keys.publicKey.export({ type: 'spki', format: 'der' }).toString('base64'); return { publicKey, actor: address(publicKey), sign: (message: string) => sign('sha256', Buffer.from(message), { key: keys.privateKey, dsaEncoding: 'ieee-p1363' }).toString('base64') }; } const seller = identity(), buyer = identity(), stranger = identity(), cardHash = 'a'.repeat(64); const evidence = `fixture:${'c'.repeat(64)}`, tx = `fixture:${'d'.repeat(64)}`; async function fixture(t: { after(fn: () => Promise): void }) { const dir = await mkdtemp(join(tmpdir(), 'wikideck-custody-')); let now = 1_800_000_000_000, present = true; const options = { feeBps: 250, now: () => now, card: (hash: string, owner: string, sequence: number) => present && hash === cardHash && owner === seller.actor && sequence === 0 ? { title: 'Custody fixture' } : undefined }; let market = await CustodyMarket.open(dir, options); t.after(async () => { market.close(); await rm(dir, { recursive: true, force: true }); }); const envelope = (who: typeof buyer, action: CustodyAction, nonce: string = randomUUID()) => { const c: Omit = { version: 1, mode: 'sandbox', policy: CUSTODY_POLICY, network: NETWORK, chainId: CHAIN_ID, custodianId: market.id, actor: who.actor, publicKey: who.publicKey, nonce, expiresAt: now + 60000, action }; return { ...c, signature: who.sign(custodyMessage(c)) }; }; const execute = (who: typeof buyer, action: CustodyAction) => market.execute(envelope(who, action)) as T; const activity = (who = buyer) => execute(who, { type: 'activity', offset: 0 }); const credit = (who = buyer) => execute(who, { type: 'mock-credit' }); const listing = (price = '100000000000') => execute(seller, { type: 'list', cardHash, sequence: 0, priceAtomicXmr: price, feeBps: 250 }); const reserve = (l: CustodyListing, who = buyer) => execute(who, { type: 'reserve', listingId: l.id, priceAtomicXmr: l.priceAtomicXmr, feeBps: l.feeBps }); return { dir, envelope, execute, activity, credit, listing, reserve, get market() { return market; }, changeCard: () => { present = false; }, advance: (ms: number) => { now += ms; }, restart: async (policy: MarketPolicy = {}) => { market.close(); market = await CustodyMarket.open(dir, { ...options, ...policy }); } }; } async function feeAddress() { const prefix = Uint8Array.from([18, ...Array(64).fill(2)]); const data = Buffer.concat([prefix, Buffer.from((await keccak(prefix, 256)).slice(0, 8), 'hex')]); const alphabet = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; const sizes: Record = { 1: 2, 2: 3, 3: 5, 4: 6, 5: 7, 6: 9, 7: 10, 8: 11 }; let encoded = ''; for (let i = 0; i < data.length; i += 8) { const chunk = data.subarray(i, i + 8); let n = BigInt(`0x${chunk.toString('hex')}`), text = ''; while (n) { text = alphabet[Number(n % 58n)] + text; n /= 58n; } encoded += text.padStart(sizes[chunk.length], '1'); } return encoded; } test('operator market switches are strict and host fee defaults to fixed author with checksum validation', async () => { assert.deepEqual(operatorMarketPolicy({}), { marketEnabled: true, tradingEnabled: true, hostFeeAddress: ADDITIONAL_MARKET_FEE.address }); for (const v of ['', 'true', 'false', 'yes', '2']) { assert.throws(() => operatorMarketPolicy({ WIKIDECK_MARKET_ENABLED: v })); assert.throws(() => operatorMarketPolicy({ WIKIDECK_MARKET_TRADING_ENABLED: v })); } await validateMarketPolicy(operatorMarketPolicy({})); await assert.rejects(validateMarketPolicy({ hostFeeAddress: '4'.repeat(95) }), /checksum|network/); }); test('market disable hides public listings and rejects new business but retains private balances and safe release', async t => { const f = await fixture(t); f.credit(); const l = f.listing(), o = f.reserve(l); const w = f.execute(buyer, { type: 'withdraw', destination: 'mock-external', amountAtomicXmr: '100', maxFeeAtomicXmr: '1' }); await f.restart({ marketEnabled: false }); assert.deepEqual(f.market.browse().listings, []); assert.equal(f.market.info().marketEnabled, false); assert.throws(() => f.listing('2'), /disabled/); assert.throws(() => f.credit(stranger), /disabled/); f.execute(buyer, { type: 'cancel-reservation', orderId: o.id }); f.execute(buyer, { type: 'cancel-withdrawal', withdrawalId: w.id }); assert.equal(f.activity().availableAtomicXmr, MOCK_CREDIT_ATOMIC); const withdrawal = f.execute(buyer, { type: 'withdraw', destination: 'mock-external', amountAtomicXmr: '1', maxFeeAtomicXmr: '0' }); assert.equal(withdrawal.status, 'reserved'); }); test('paused trading keeps browse and cancellations available, and fee recipients are signed and snapshotted', async t => { const f = await fixture(t), recipient = await feeAddress(); f.credit(); await f.restart({ hostFeeAddress: recipient }); assert.throws(() => f.listing(), /recipient/); const action = { type: 'list' as const, cardHash, sequence: 0, priceAtomicXmr: '1000', feeBps: 250, hostFeeAddress: recipient }; const c = f.envelope(seller, action); assert.throws(() => f.market.execute({ ...c, action: { ...action, hostFeeAddress: ADDITIONAL_MARKET_FEE.address } }), /signature/); const l = f.market.execute(c) as CustodyListing; await f.restart({ hostFeeAddress: ADDITIONAL_MARKET_FEE.address, tradingEnabled: false }); assert.equal(f.market.browse().listings[0].hostFeeAddress, recipient); assert.throws(() => f.reserve(l), /disabled/); await f.restart(); assert.throws(() => f.reserve(l), /recipient/); const o = f.execute(buyer, { type: 'reserve', listingId: l.id, priceAtomicXmr: l.priceAtomicXmr, feeBps: 250, hostFeeAddress: recipient }); assert.equal(o.hostFeeAddress, recipient); assert.equal(o.authorAddress, ADDITIONAL_MARKET_FEE.address); f.market.settleFixture(o.id, evidence); assert.equal(f.market.audit().assetsAtomicXmr, MOCK_CREDIT_ATOMIC); }); test('custody mode is off by default and rejects all live configurations', () => { assert.equal(custodyMode({}), 'off'); assert.equal(custodyMode({ WIKIDECK_CUSTODY_MODE: 'sandbox' }), 'sandbox'); for (const mode of ['live', 'mainnet', '', 'processor']) assert.throws(() => custodyMode({ WIKIDECK_CUSTODY_MODE: mode })); }); test('custody requires a separate consent domain and rejects malformed monetary encodings', async t => { const f = await fixture(t), c = f.envelope(buyer, { type: 'mock-credit' }); assert.match(custodyMessage(c), /wikideck-custody-command-v1/); for (const patch of [{ actor: seller.actor }, { chainId: 'f'.repeat(64) }, { custodianId: randomUUID() }, { expiresAt: c.expiresAt + 1 }, { action: { type: 'withdraw', destination: 'mock-external', amountAtomicXmr: '1', maxFeeAtomicXmr: '0' } }]) { assert.throws(() => f.market.execute({ ...c, ...patch })); } for (const value of ['0', '-1', '01', '1.0', '1e2', ' 1', '1\n', '999999999999999999999999999999999']) { assert.equal(custodyCommandSchema.safeParse({ ...c, action: { type: 'withdraw', destination: 'mock-external', amountAtomicXmr: value, maxFeeAtomicXmr: '0' } }).success, false); } assert.throws(() => f.market.execute({ ...c, mode: 'live' })); assert.throws(() => f.market.execute({ ...c, action: { type: 'withdraw', destination: 'real-address', amountAtomicXmr: '1', maxFeeAtomicXmr: '0' } })); assert.equal(f.activity().availableAtomicXmr, '0'); }); test('mock funding and replay receipts persist across restart and never credit twice', async t => { const f = await fixture(t), c = f.envelope(buyer, { type: 'mock-credit' }); const first = f.market.execute(c); assert.deepEqual(f.market.execute(c), first); assert.equal(f.activity().availableAtomicXmr, MOCK_CREDIT_ATOMIC); assert.throws(() => f.credit(), /already claimed/); const changed = f.envelope(buyer, { type: 'withdraw', destination: 'mock-external', amountAtomicXmr: '1', maxFeeAtomicXmr: '0' }, c.nonce); assert.throws(() => f.market.execute(changed), /Nonce already used/); await f.restart(); assert.deepEqual(f.market.execute(c), first); assert.equal(f.market.audit().journals, 1); f.advance(60001); assert.throws(() => f.market.execute(c), /expired/); }); test('funded fixed-price reservation and blocked intent commit together with private activity', async t => { const f = await fixture(t), l = f.listing(); assert.throws(() => f.reserve(l), /Insufficient/); assert.equal(f.market.browse().listings[0].status, 'open'); assert.equal(f.activity().orders.length, 0); f.credit(); const order = f.reserve(l); assert.equal(order.status, 'awaiting-lock'); assert.equal(order.sellerAtomicXmr, '96500000000'); assert.equal(order.authorAtomicXmr, '1000000000'); assert.equal(order.hostAtomicXmr, '2500000000'); assert.equal(f.activity().availableAtomicXmr, '900000000000'); assert.equal(f.activity().purchaseReservedAtomicXmr, l.priceAtomicXmr); assert.equal(f.activity(seller).availableAtomicXmr, '0'); assert.equal(f.activity(seller).orders.length, 1); assert.equal(f.activity(stranger).orders.length, 0); assert.throws(() => f.execute(stranger, { type: 'cancel-reservation', orderId: order.id }), /Only the buyer/); await f.restart(); f.execute(buyer, { type: 'cancel-reservation', orderId: order.id }); assert.equal(f.activity().availableAtomicXmr, MOCK_CREDIT_ATOMIC); assert.equal(f.activity().purchaseReservedAtomicXmr, '0'); assert.throws(() => f.market.settleFixture(order.id, evidence), /not eligible/); assert.equal(f.market.audit().assetsAtomicXmr, MOCK_CREDIT_ATOMIC); }); test('card changes, self-purchases and changed amounts cannot reserve funds', async t => { const f = await fixture(t), l = f.listing(); f.credit(); assert.throws(() => f.reserve(l, seller), /own card/); assert.throws(() => f.execute(buyer, { type: 'reserve', listingId: l.id, priceAtomicXmr: '1', feeBps: 250 }), /terms changed/); f.changeCard(); assert.throws(() => f.reserve(l), /no longer available/); assert.equal(f.activity().availableAtomicXmr, MOCK_CREDIT_ATOMIC); }); test('offline canonical fixture settles exactly once with conserved fees and one-atomic-unit prices', async t => { const f = await fixture(t); f.credit(); const l = f.listing('1'), o = f.reserve(l); assert.equal(o.sellerAtomicXmr, '1'); assert.equal(o.hostAtomicXmr, '0'); assert.equal(o.authorAtomicXmr, '0'); assert.throws(() => f.market.settleFixture(o.id, 'rpc-confirmed'), /offline fixture/); f.market.settleFixture(o.id, evidence); assert.equal(f.activity(seller).availableAtomicXmr, '1'); assert.equal(f.activity().availableAtomicXmr, '999999999999'); await f.restart(); f.market.settleFixture(o.id, evidence); assert.equal(f.activity(seller).availableAtomicXmr, '1'); assert.throws(() => f.market.settleFixture(o.id, `fixture:${'b'.repeat(64)}`), /Conflicting/); assert.throws(() => f.execute(buyer, { type: 'cancel-reservation', orderId: o.id }), /Only the buyer/); assert.deepEqual(f.market.audit(), { journals: 3, assetsAtomicXmr: MOCK_CREDIT_ATOMIC, claimsAndRevenueAtomicXmr: MOCK_CREDIT_ATOMIC }); }); test('competing signed reservations serialize and failed attempts leave no partial writes', async t => { const f = await fixture(t), l = f.listing('800000000000'); f.credit(); f.credit(stranger); const commands = [f.envelope(buyer, { type: 'reserve', listingId: l.id, priceAtomicXmr: l.priceAtomicXmr, feeBps: 250 }), f.envelope(stranger, { type: 'reserve', listingId: l.id, priceAtomicXmr: l.priceAtomicXmr, feeBps: 250 })]; const results = await Promise.allSettled(commands.map(c => Promise.resolve().then(() => f.market.execute(c)))); assert.equal(results.filter(r => r.status === 'fulfilled').length, 1); assert.equal(f.activity().orders.length + f.activity(stranger).orders.length, 1); assert.equal(f.market.audit().journals, 3); assert.throws(() => f.execute(buyer, { type: 'withdraw', destination: 'mock-external', amountAtomicXmr: '300000000000', maxFeeAtomicXmr: '0' }), /Insufficient/); assert.equal(f.activity().withdrawals.length, 0); }); test('withdrawal cancellation releases maximum fee and prepared uncertainty blocks cancellation/replacement', async t => { const f = await fixture(t); f.credit(); const make = () => f.execute(buyer, { type: 'withdraw', destination: 'mock-external', amountAtomicXmr: '100000000000', maxFeeAtomicXmr: '1000' }); const cancelled = make(); f.execute(buyer, { type: 'cancel-withdrawal', withdrawalId: cancelled.id }); assert.equal(f.activity().availableAtomicXmr, MOCK_CREDIT_ATOMIC); const w = make(), attempt = randomUUID(); assert.throws(() => f.market.prepareWithdrawalFixture(w.id, attempt, tx, '1001'), /fee cap/); assert.equal(f.activity().withdrawals[0].status, 'reserved'); f.market.prepareWithdrawalFixture(w.id, attempt, tx, '400'); await f.restart(); assert.throws(() => f.execute(buyer, { type: 'cancel-withdrawal', withdrawalId: w.id }), /unprepared/); assert.throws(() => f.market.prepareWithdrawalFixture(w.id, randomUUID(), tx, '400'), /different terms/); assert.equal(f.market.prepareWithdrawalFixture(w.id, attempt, tx, '400').transactionId, tx); assert.throws(() => f.market.confirmWithdrawalFixture(w.id, `fixture:${'e'.repeat(64)}`), /mismatch/); f.market.confirmWithdrawalFixture(w.id, tx); f.market.confirmWithdrawalFixture(w.id, tx); assert.equal(f.activity().availableAtomicXmr, '899999999600'); assert.equal(f.activity().withdrawalReservedAtomicXmr, '0'); assert.equal(f.market.audit().assetsAtomicXmr, '899999999600'); }); test('multiple connections cannot overspend a balance and cancelled commands do not double-release', async t => { const f = await fixture(t); f.credit(); const second = await CustodyMarket.open(f.dir, { feeBps: 250, now: () => 1_800_000_000_000, card: () => undefined }); try { const c = f.envelope(buyer, { type: 'withdraw', destination: 'mock-external', amountAtomicXmr: '900000000000', maxFeeAtomicXmr: '0' }); const w = second.execute(c) as CustodyWithdrawal; assert.throws(() => f.execute(buyer, { type: 'withdraw', destination: 'mock-external', amountAtomicXmr: '900000000000', maxFeeAtomicXmr: '0' }), /Insufficient/); const cancel = f.envelope(buyer, { type: 'cancel-withdrawal', withdrawalId: w.id }); f.market.execute(cancel); second.execute(cancel); assert.equal(f.activity().availableAtomicXmr, MOCK_CREDIT_ATOMIC); second.audit(); } finally { second.close(); } }); test('sandbox cap stops new work but never blocks release of existing unstarted holds', async t => { const f = await fixture(t); f.credit(); const w = f.execute(buyer, { type: 'withdraw', destination: 'mock-external', amountAtomicXmr: '100', maxFeeAtomicXmr: '1' }); const db = new DatabaseSync(join(f.dir, 'testnet', 'custody-sandbox.sqlite')); try { const insert = db.prepare('INSERT INTO receipts VALUES(?,?,?)'); db.exec('BEGIN'); for (let i = 0; i < 10000; i++) insert.run(`fixture:${i}`, 'fixture', '{}'); db.exec('COMMIT'); assert.throws(() => f.listing(), /budget/); f.execute(buyer, { type: 'cancel-withdrawal', withdrawalId: w.id }); assert.equal(f.activity().availableAtomicXmr, MOCK_CREDIT_ATOMIC); assert.throws(() => db.prepare("UPDATE entries SET debit='1'").run(), /Immutable/); assert.throws(() => db.prepare('DELETE FROM journals').run(), /Immutable/); db.prepare("UPDATE accounts SET balance='12' WHERE id=?").run(`customer:${buyer.actor}:available`); assert.throws(() => f.market.audit(), /projection mismatch/); } finally { db.close(); } }); test('property-style repeated settlements conserve gross and never round up author/host fees', async t => { const f = await fixture(t); f.credit(); let spent = 0n, received = 0n; for (let i = 1n; i <= 40n; i++) { const price = i * 137n, o = f.reserve(f.listing(price.toString())); f.market.settleFixture(o.id, `fixture:${i.toString(16).padStart(64, '0')}`); assert.equal(BigInt(o.authorAtomicXmr), price / 100n); assert.equal(BigInt(o.hostAtomicXmr), price * 250n / 10000n); spent += price; received += BigInt(o.sellerAtomicXmr); assert.equal(BigInt(f.activity().availableAtomicXmr) + spent, BigInt(MOCK_CREDIT_ATOMIC)); assert.equal(BigInt(f.activity(seller).availableAtomicXmr), received); assert.equal(f.market.audit().assetsAtomicXmr, MOCK_CREDIT_ATOMIC); } });