import { useCallback, useEffect, useRef, useState } from 'react'; import { NETWORK, CONSENSUS, testnetOwnerOf, transactionCardIds, type TestnetSnapshot } from '../shared/testnet.ts'; import { ADDITIONAL_MARKET_FEE } from '../shared/market.ts'; import { CUSTODY_POLICY, custodyMessage, type CustodyAction, type CustodyActivity, type CustodyCommand, type CustodyPage } from '../shared/custody.ts'; import { formatXmr, parseXmr } from '../shared/xmr-amount.ts'; import { sign, type Wallet } from './wallet.ts'; import './custody.css'; type Props = { wallet?: Wallet; snapshot?: TestnetSnapshot; rpcAvailable: boolean }; const short = (s: string) => `${s.slice(0, 8)}…${s.slice(-5)}`; // Aggregate balances have a wider bound than individual sale amounts; format without Number coercion. const display = (value: string) => { const n = BigInt(value); return `${n / 1_000_000_000_000n}.${(n % 1_000_000_000_000n).toString().padStart(12, '0')}`; }; async function request(command?: CustodyCommand, offset = 0): Promise { const response = await fetch(command ? '/api/custody/command' : `/api/custody?offset=${offset}`, { cache: 'no-store', signal: AbortSignal.timeout(15000), ...(command ? { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(command), } : {}), }); if (!response.headers.get('content-type')?.includes('application/json')) throw new Error('Custody API unavailable.'); const data = await response.json(); if (!response.ok) throw new Error(typeof data.error === 'string' ? data.error : 'Custody request failed.'); return data; } export function CustodyPanel({ wallet, snapshot, rpcAvailable }: Props) { const [page, setPage] = useState(); const [activity, setActivity] = useState(); const [enabled, setEnabled] = useState(); const [busy, setBusy] = useState(false); const [error, setError] = useState(''); const [notice, setNotice] = useState(''); const [review, setReview] = useState<{ action: CustodyAction; description: string }>(); const [retry, setRetry] = useState(); const [card, setCard] = useState(''); const [price, setPrice] = useState('0.100000000000'); const [withdraw, setWithdraw] = useState('0.100000000000'); const [maxFee, setMaxFee] = useState('0.001000000000'); const generation = useRef(0), pending = useRef(false); const reviewElement = useRef(null); const signed = useCallback(async (action: CustodyAction, current: CustodyPage): Promise => { if (!wallet) throw new Error('Open your wallet first.'); const c: Omit = { version: 1, mode: 'sandbox', policy: CUSTODY_POLICY, network: NETWORK, chainId: current.info.chainId, custodianId: current.info.custodianId, actor: wallet.address, publicKey: wallet.publicKey, nonce: crypto.randomUUID(), expiresAt: Date.now() + 4 * 60_000, action }; return { ...c, signature: await sign(wallet, custodyMessage(c)) }; }, [wallet]); const refresh = useCallback(async (offset = 0) => { const epoch = generation.current; const data = await request(undefined, offset); if (epoch !== generation.current) return; if ('enabled' in data) { setEnabled(false); setPage(undefined); setActivity(undefined); return; } if (data.info.mode !== 'sandbox' || data.info.policy !== CUSTODY_POLICY || data.info.network !== NETWORK || data.info.liveDeposits !== false || data.info.liveWithdrawals !== false || data.info.nftSettlement !== false || (snapshot && data.info.chainId !== snapshot.chainId)) throw new Error('Incompatible custody policy or network.'); const command = wallet ? await signed({ type: 'activity', offset }, data) : undefined; if (epoch !== generation.current) return; const mine = command ? await request(command) : undefined; if (epoch !== generation.current) return; setEnabled(true); setPage(data); setActivity(mine); }, [wallet, signed, snapshot?.chainId]); useEffect(() => { generation.current++; setPage(undefined); setActivity(undefined); setReview(undefined); setRetry(undefined); setError(''); setBusy(false); if (rpcAvailable) void refresh().catch(e => setError(e instanceof Error ? e.message : 'Custody unavailable.')); return () => { generation.current++; }; }, [refresh, rpcAvailable]); useEffect(() => { if (review) reviewElement.current?.focus(); }, [review]); const prepare = (action: CustodyAction, description: string) => { if (['list','reserve','mock-credit'].includes(action.type) && (page?.info.marketEnabled === false || page?.info.tradingEnabled === false)) { setError('This host is not accepting new trading or funding. Existing balances and cancellation/withdrawal requests remain accessible.'); return; } if (action.type === 'list' || action.type === 'reserve') { const listingId = action.type === 'reserve' ? action.listingId : undefined; const recipient = action.type === 'reserve' ? page?.listings.find(l => l.id === listingId)?.hostFeeAddress ?? ADDITIONAL_MARKET_FEE.address : page?.info.hostFeeAddress ?? ADDITIONAL_MARKET_FEE.address; action = { ...action, hostFeeAddress: recipient }; description += ` Host fee recipient: ${recipient}. The separate fixed author fee is unchanged.`; } setError(''); setNotice(''); setReview({ action, description }); }; const submit = async (again?: CustodyCommand) => { if (!wallet || !page || pending.current || (!review && !again)) return; pending.current = true; setBusy(true); setError(''); const epoch = generation.current; let command: CustodyCommand | undefined; try { command = again ?? await signed(review!.action, page); if (epoch !== generation.current) return; setRetry(command); // Retain the EXACT envelope after ambiguous network outcomes; do not mint another debit nonce. await request(command); if (epoch !== generation.current) return; setReview(undefined); setRetry(undefined); setNotice('Sandbox command recorded. No XMR or NFT was transferred.'); await refresh(); } catch (e) { if (epoch === generation.current) setError(`${e instanceof Error ? e.message : 'Custody request failed.'}${command ? ' Refresh activity before making another request; retry uses the original signed command.' : ''}`); } finally { pending.current = false; if (epoch === generation.current) setBusy(false); } }; const owned = snapshot?.blocks.filter(b => wallet && testnetOwnerOf(snapshot, b.tokenId) === wallet.address && snapshot.blocks.length - b.height + 1 >= CONSENSUS.coinbaseMaturity && !snapshot.pendingTransfers.some(t => transactionCardIds(t).includes(b.tokenId))) ?? []; const disabled = busy || !wallet || !rpcAvailable || !page || !!retry; return
Custodial ledger preview · fictitious balances only

Test funding, fixed-price reservations and withdrawal holds. No deposit address, Monero access, NFT lock or card delivery is enabled. Existing mock checkout is separate. Funds shown here have no monetary value.

A balance for this market.

A future custodian would hold pooled XMR and track what each customer is owed. Purchases would move ledger balances, not XMR between subaddresses. You would trust the operator with your deposits.

{enabled === false &&

This host has not enabled the custody sandbox. Real custody is unavailable.

} {page&&(page.info.marketEnabled===false||page.info.tradingEnabled===false)&&

{page.info.marketEnabled===false?'Public market disabled on this host.':'New trading paused on this host.'} Existing balances, cancellations and withdrawal requests remain accessible.

} {page?.info.hostFeeAddress&&

Host fee wallet (not a deposit address): {page.info.hostFeeAddress}

} {!rpcAvailable &&

Reconnect to the node to use this preview.

} {!wallet &&

Open your Wikideck wallet to view private balances and authorize sandbox commands.

} {page &&

Sandbox custodian {page.info.custodianId} · {page.info.network} · host {page.info.feeBps / 100}% + author 1%. Fresh custody signatures are required; legacy mock authorizations do not debit balances.

} {error &&

{error}

} {notice &&

{notice}

}
{retry && <> }
{activity && <>
{[ ['Available', activity.availableAtomicXmr], ['Purchase reserved', activity.purchaseReservedAtomicXmr], ['Withdrawal reserved', activity.withdrawalReservedAtomicXmr], ].map(([label, value]) =>
{label}{display(value)} XMRSIMULATED
)}
} {review &&

Review sandbox authorization

{review.description}

} {page && <>
{ e.preventDefault(); try { if (!snapshot) throw new Error('Chain snapshot unavailable.'); const priceAtomicXmr = parseXmr(price).toString(); if (BigInt(priceAtomicXmr) === 0n) throw new Error('Use a positive price.'); prepare({ type: 'list', cardHash: card, sequence: snapshot.transfers.filter(t => transactionCardIds(t).includes(card)).length, priceAtomicXmr, feeBps: page.info.feeBps }, `List this card for ${formatXmr(BigInt(priceAtomicXmr))} fictitious XMR. Host ${page.info.feeBps / 100}% plus author 1% are deducted from gross if settlement is implemented. This preview does not lock or transfer the card.`); } catch (e) { setError(e instanceof Error ? e.message : 'Invalid listing.'); } }}>

Fixed-price custody listing

setPrice(e.target.value)}/>
{ e.preventDefault(); try { const amountAtomicXmr = parseXmr(withdraw).toString(), maxFeeAtomicXmr = parseXmr(maxFee).toString(); if (BigInt(amountAtomicXmr) === 0n) throw new Error('Use a positive withdrawal amount.'); prepare({ type: 'withdraw', destination: 'mock-external', amountAtomicXmr, maxFeeAtomicXmr }, `Reserve ${withdraw} fictitious XMR plus up to ${maxFee} XMR fee allowance for a MOCK destination. No address is accepted, no XMR is sent, and this hold remains cancellable until prepared by an offline test fixture.`); } catch (e) { setError(e instanceof Error ? e.message : 'Invalid withdrawal.'); } }}>

Withdrawal reservation preview

setWithdraw(e.target.value)}/> setMaxFee(e.target.value)}/>

Custody listings · no auctions

{page.listings.map(l =>

{l.title}

{display(l.priceAtomicXmr)} XMR · {l.status} · seller {short(l.seller)}

{l.status === 'open' && (l.seller === wallet?.address ? : )}
)}
{page.next !== null && } } {activity &&

Your custody activity

{activity.orders.map(o =>
Order {short(o.id)}

{display(o.grossAtomicXmr)} XMR · {o.status} · {o.buyer === wallet?.address ? 'Buyer' : 'Seller'}

{o.status === 'awaiting-lock' &&

Blocked: NFT consensus lock and settlement are not implemented. No delivery signature was issued.

} {o.buyer === wallet?.address && o.status === 'awaiting-lock' && }
)} {activity.withdrawals.map(w =>
Mock withdrawal {short(w.id)}

{display(w.amountAtomicXmr)} XMR + max {display(w.maxFeeAtomicXmr)} fee · {w.status}

{w.status === 'reserved' && }
)} {activity.next !== null && }
}

Author policy recipient (not a deposit address): {ADDITIONAL_MARKET_FEE.address}. No real funds or commissions are collected.

; }