Documentation and UI

This commit is contained in:
2026-10-20 10:40:18 -04:00
parent f2bd3e38e2
commit 2814b46b73
8 changed files with 105 additions and 23 deletions
+1
View File
File diff suppressed because one or more lines are too long
+21 -6
View File
@@ -166,7 +166,7 @@ export default function App() {
const Icon = [LayoutDashboard, Layers3, Globe2, ArrowLeftRight, ShoppingBag][i];
return <button key={item} aria-label={item} aria-current={view === item ? 'page' : undefined} className={`nav-item ${view === item ? 'active' : ''}`} onClick={() => { setView(item); setQuery(''); setCategory('All cards'); }}><Icon size={18} />{item}{item === 'My collection' && <span className="nav-count">{owned.length}</span>}</button>;
})}</nav>
<div className="sidebar-note"><span className="tiny-spark">✦</span><h3>A little curiosity.<br />An independent chain.</h3><p>Collect knowledge as native NFTs on a sovereign RandomX testnet.</p><button onClick={() => setDialog('guide')}>How it works <ArrowUpRight size={15} /></button></div>
<div className="sidebar-note"><span className="tiny-spark">✦</span><h3>Your cards.<br />Your community.</h3><p>Mine Wikipedia NFT cards, build collections, and trade in peer-hosted markets.</p><button onClick={() => setDialog('guide')}>How it works <ArrowUpRight size={15} /></button></div>
<div className="sidebar-bottom"><span className={`network-dot ${!rpcAvailable || !snapshot?.peers ? 'disconnected' : ''}`} /><span>{peerLabel}</span><span className="prototype">{NETWORK_LABEL}</span></div>
<button className="help-link" onClick={() => setDialog('guide')}><CircleHelp size={16} /> The field guide <ArrowUpRight size={14} /></button>
</aside>
@@ -177,7 +177,7 @@ export default function App() {
{error && <div className="error-banner" role="alert"><CircleHelp size={17} /><span>{error}</span><button onClick={retry}>Retry</button><button aria-label="Dismiss error" onClick={() => setError('')}><X size={16} /></button></div>}
{view === 'Discover' && <>
<section className="hero">
<div className="hero-copy"><span className="eyebrow"><span /> KNOWLEDGE, COLLECTED ON ITS OWN CHAIN.</span><h1>Mine knowledge.<br />Collect the <span>extraordinary.</span></h1><p>Random Wikipedia articles, each with a picture. A chain of new discoveries. Mine native NFTs with an opt-in RandomX miner, and keep the keys to your collection.</p><div className="hero-actions"><button className="button dark" onClick={goMine}><Pickaxe size={17} /> Explore native mining <ArrowUpRight size={17} /></button><button className="text-button" onClick={() => setDialog('guide')}>How it works <ArrowRight size={16} /></button></div><div className="hero-foot"><span><Check size={13} /> Independent testnet</span><span><Check size={13} /> No browser mining</span></div></div>
<div className="hero-copy"><span className="eyebrow"><span /> WIKIPEDIA CARDS. THEIR OWN NFT BLOCKCHAIN.</span><h1>Mine knowledge.<br />Collect the <span>extraordinary.</span></h1><p>Mine a block. Earn a Wikipedia NFT card. Build collections, trade discoveries, and find your community in peer-hosted markets. Trading card games are next on the roadmap.</p><div className="hero-actions"><button className="button dark" onClick={goMine}><Pickaxe size={17} /> Start collecting <ArrowUpRight size={17} /></button><button className="text-button" onClick={() => setDialog('guide')}>How it works <ArrowRight size={16} /></button></div><div className="hero-foot"><span><Check size={13} /> Consumer-CPU mining</span><span><Check size={13} /> Peer-hosted markets</span></div></div>
<div className="hero-visual"><div className="orbit orbit-one" /><div className="orbit orbit-two" /><span className="orbit-spark spark-one">✳</span><span className="orbit-spark spark-two">+</span><div className="floating-label label-top"><Sparkles size={12} /> Preview cards · not owned</div><div className="hero-card back-card"><Card card={samples[2]} onClick={() => setSelected(samples[2])} /></div><div className="hero-card front-card"><Card card={samples[0]} onClick={() => setSelected(samples[0])} /></div><div className="floating-label label-bottom"><Sparkles size={15} /> A world worth collecting <ArrowUpRight size={13} /></div></div>
</section>
<section className="metrics" aria-label="Canonical chain statistics">
@@ -187,9 +187,14 @@ export default function App() {
<div><span className="metric-icon"><Cpu size={19} /></span><div><span>Proof of work</span><strong className="algorithm">{snapshot?.algorithm ?? 'RandomX'}<small>native CPU mining</small></strong></div></div>
</section>
<section className="mining-panel native-mining" ref={minerPanel}>
<div className="mining-description"><div className="section-eyebrow"><span className="idle-dot" /> EXPLICIT OPT-IN · OUTSIDE YOUR BROWSER</div><h2>Native RandomX mining</h2><p>This page never runs mining code or launches a native program. Build the trusted native binary, download an encrypted wallet backup, then start the miner yourself in an interactive terminal.</p><div className="mining-meta"><span><Cpu size={13} /> One native worker</span><span><Shield size={13} /> Independent validating peers</span></div><div className="native-memory"><strong>Light · default</strong><span>256 MiB cache + scratchpad and overhead.</span><strong>Fast · explicit --fast</strong><span>~2 GiB dataset plus cache; budget over 2.3 GiB per process. Higher CPU, energy and memory use. RandomX is not a promise of ASIC-proof mining.</span></div></div>
<div className="mining-description"><div className="section-eyebrow"><span className="idle-dot" /> RANDOMX · BUILT FOR GENERAL-PURPOSE CPUS</div><h2>Mine blocks. Earn cards.</h2><p>Wikideck uses RandomX, the proof-of-work algorithm also used by Monero, to keep mining competitive on consumer-grade CPUs. Each accepted block rewards its miner with an illustrated Wikipedia NFT card. Results depend on your hardware and network difficulty; Wikideck does not share Monero’s network security.</p><div className="mining-meta"><span><Cpu size={13} /> Consumer hardware</span><span><Shield size={13} /> Independent validating peers</span></div><div className="native-memory"><strong>Light · default</strong><span>256 MiB cache + scratchpad and overhead.</span><strong>Fast · --fast</strong><span>~2 GiB dataset plus cache; budget over 2.3 GiB per process. Mining consumes CPU, memory and energy; RandomX is not a guarantee of ASIC resistance or profitability.</span></div></div>
<div className="miner-controls"><div className="miner-display"><div><span>NEXT WIKIPEDIA ARTICLE</span><strong>{snapshot ? 'Chosen when the miner requests work' : 'Waiting for testnet RPC…'}</strong></div></div><p className="muted">Each mining template requests a random English Wikipedia article with a Wikimedia picture. Pages may repeat; every minted card carries its own statistics snapshot.</p><ol className="native-steps"><li>Build <code>wikideck-randomx</code> using the repository’s native build instructions.</li><li>Save your encrypted backup and keep its password separate.</li><li>Replace <code>&lt;backup.json&gt;</code> with your local file path (quote paths with spaces), then run:</li></ol><pre className="native-command"><code>{nativeCommand}</code></pre><p className="miner-footnote">Node: {window.location.origin}. Same-origin RPC, including the development proxy. Use HTTPS for remote nodes. Password entry stays local; never put it in the command. Defaults to one worker and at most one block. Ctrl+C stops the native miner; closing this page does not.</p><button className="button dark full" onClick={openWallet}><Download size={16} /> Download wallet backup</button><button className="text-button" onClick={() => setDialog('guide')}>Read the testnet field guide <ArrowRight size={14} /></button></div>
</section>
<section className="project-pillars" aria-label="Collect, host, and play">
<article><Layers3 size={22}/><div className="section-eyebrow">COLLECT & TRADE</div><h2>A collection with a story.</h2><p>Collect Wikipedia pages as cards, swap multiple cards in one trade, or list discoveries at a fixed price or timed auction.</p><button className="text-button" onClick={()=>setView('Trade decks')}>Trade cards <ArrowRight size={14}/></button></article>
<article><Globe2 size={22}/><div className="section-eyebrow">HOST YOUR OWN MARKET</div><h2>Your peer. Your community.</h2><p>Run a peer and host a market for everyone or a community centered on a collection. Sales allocate a hosting fee to the operator and a hardcoded 1% to the project author. Cryptocurrency checkout is designed for XMR; today payments and fees are simulated.</p><button className="text-button" onClick={()=>setView('Marketplace')}>Explore this peer’s market <ArrowRight size={14}/></button></article>
<article><Sparkles size={22}/><div className="section-eyebrow">ON THE ROADMAP · NOT PLAYABLE YET</div><h2>Cards made for more.</h2><p>Trading card games and peer-hosted custom TCGs are planned. The open-source direction is to let communities adapt markets and games; a software license still needs to be published. Built-in collection curation and custom game hosting are not available yet.</p><button className="text-button" onClick={()=>setDialog('guide')}>The project vision <ArrowRight size={14}/></button></article>
</section>
</>}
{view === 'Marketplace' ? <Marketplace wallet={wallet} snapshot={snapshot} rpcAvailable={rpcAvailable} onNotice={setToast} /> : view === 'Trade decks' ? <TradeDesk wallet={wallet} snapshot={snapshot} rpcAvailable={rpcAvailable} onNotice={setToast} /> : view !== 'Block explorer' ? <section className="discover-section">
<div className="section-heading"><div><div className="section-eyebrow">{view === 'My collection' ? 'YOUR PERSONAL ENCYCLOPEDIA' : 'AN ENCYCLOPEDIA, REIMAGINED'}</div><h2>{view === 'My collection' ? 'Your cabinet of curiosities' : 'A world of possible discoveries'}<span className="count-badge">{visibleCards.length}</span></h2><p>{view === 'My collection' ? 'Confirmed native NFT ownership. Pending transfers stay here until mined.' : snapshot?.blocks.length ? 'Recent native NFTs on this node’s canonical testnet chain.' : 'Sample previews only; mined NFTs use random Wikipedia pages with pictures. The same page may be minted more than once.'}</p></div>{view === 'Discover' && <button className="text-button" onClick={() => setView('My collection')}>My collection <ArrowRight size={16} /></button>}</div>
@@ -198,7 +203,7 @@ export default function App() {
</section> : <section className="explorer-section"><div className="section-eyebrow">FOLLOW THE CANONICAL CHAIN</div><div className="section-heading"><div><h1>Block explorer<span className="count-badge">{snapshot?.blocks.length ?? 0}</span></h1><p>Native NFT mints, RandomX proofs, and mined ownership transfers.</p></div><a className="button outline" href="/api/chain" target="_blank" rel="noreferrer">View full chain JSON <ExternalLink size={15} /></a></div>
<div className="explorer-note"><Shield size={18} /><p>Sovereign testnet with independently validating peers and cumulative-work chain selection. This network is unbootstrapped, not a public mainnet. Zero peers means an isolated node, not a connected network. Canonical ownership can change during a reorganization.</p></div>
<dl className="chain-summary"><div><dt>Cumulative work</dt><dd>{snapshot?.cumulativeWork ?? '—'}</dd></div><div><dt>Peer count</dt><dd>{rpcAvailable ? snapshot?.peers ?? '—' : 'Unknown'} · {peerLabel}</dd></div><div><dt>Chain ID · pinned genesis</dt><dd>{snapshot?.chainId ?? '—'}</dd></div><div><dt>Network</dt><dd>{NETWORK}</dd></div><div><dt>Tip block ID</dt><dd>{snapshot?.tip ?? '—'}</dd></div><div><dt>Mint maturity</dt><dd>{snapshot?.maturity ?? CONSENSUS.coinbaseMaturity} subsequent blocks at inclusion height</dd></div></dl>
{snapshot?.blocks.length ? <div className="table-scroll"><table><thead><tr><th>Block / confirmations</th><th>Discovery</th><th>Block ID / NFT token ID</th><th>Confirmed owner</th><th>RandomX proof / rarity</th></tr></thead><tbody>{[...snapshot.blocks].reverse().map(b => <tr key={b.hash}><td><button className="explorer-card-link" onClick={() => setSelected(asCard(b))}>#{b.height.toString().padStart(4, '0')}</button><small>{snapshot.blocks.length - b.height + 1} confirmations</small></td><td><strong>{b.article.title}</strong><small>Wikipedia #{b.article.pageId}</small></td><td className="mono"><span title={b.hash}>Block {short(b.hash)}</span><a className="nft-link" href={`/api/nfts/${encodeURIComponent(b.tokenId)}`} target="_blank" rel="noreferrer" title={b.tokenId}>NFT {short(b.tokenId)} <ExternalLink size={11} /></a></td><td className="mono" title={testnetOwnerOf(snapshot, b.tokenId)}>{short(testnetOwnerOf(snapshot, b.tokenId) ?? '')}</td><td><span className="proof-caption mono" title={b.powHash}>{short(b.powHash)}</span><span className={`rarity-tag ${rarity(b.powHash).toLowerCase()}`}>{rarity(b.powHash)}</span></td></tr>)}</tbody></table></div> : <div className="empty-state"><img className="empty-state-logo" src="/Wikideck.png" alt="Wikideck" /><h3>A new chain. A blank page.</h3><p>No canonical NFT blocks loaded. Native mining is an explicit, separate step.</p><button className="button dark" onClick={goMine}>Explore native mining <ArrowRight size={16} /></button></div>}
{snapshot?.blocks.length ? <div className="table-scroll"><table><thead><tr><th>Block / confirmations</th><th>Discovery</th><th>Block ID / NFT token ID</th><th>Confirmed owner</th><th>RandomX proof / rarity</th></tr></thead><tbody>{[...snapshot.blocks].reverse().map(b => <tr key={b.hash}><td><button className="explorer-card-link" onClick={() => setSelected(asCard(b))}>#{b.height.toString().padStart(4, '0')}</button><small>{snapshot.blocks.length - b.height + 1} confirmations</small></td><td><strong>{b.article.title}</strong><small>Wikipedia #{b.article.pageId}</small></td><td className="mono"><span title={b.hash}>Block {short(b.hash)}</span><a className="nft-link" href={`/api/nfts/${encodeURIComponent(b.tokenId)}`} target="_blank" rel="noreferrer" title={b.tokenId}>NFT {short(b.tokenId)} <ExternalLink size={11} /></a></td><td className="mono" title={testnetOwnerOf(snapshot, b.tokenId)}>{short(testnetOwnerOf(snapshot, b.tokenId) ?? '')}</td><td><span className="proof-caption mono" title={b.powHash}>{short(b.powHash)}</span><span className={`rarity-tag ${rarity(b.powHash).toLowerCase()}`}>{rarity(b.powHash)}</span></td></tr>)}</tbody></table></div> : <div className="empty-state"><img className="empty-state-logo" src="/Wikideck.png" alt="Wikideck" /><h3>A new chain. A blank page.</h3><p>No NFT blocks loaded yet. Mine a block to earn the next Wikipedia card.</p><button className="button dark" onClick={goMine}>Explore RandomX mining <ArrowRight size={16} /></button></div>}
<h2 className="transfer-title">Mined transfers and trades</h2>{snapshot?.transfers.length ? <div className="journal">{snapshot.transfers.map((tx, i) => isNftTrade(tx)
? <div key={`trade-${tx.proposerSignature}`}><ArrowLeftRight size={18}/><span>{tx.offered.length} cards {short(tx.proposer)} ⇄ {tx.requested.length} cards {short(tx.counterparty)}</span><span>Atomic trade · mined #{snapshot.blocks.find(b => b.transactions.some(item => isNftTrade(item) && item.proposerSignature === tx.proposerSignature))?.height ?? '—'}</span><Check size={14} aria-label={`Mined trade ${i + 1}`}/></div>
: <div key={`${tx.cardHash}-${tx.sequence}`}><ArrowDownLeft size={18}/><a href={`/api/nfts/${encodeURIComponent(tx.cardHash)}`} target="_blank" rel="noreferrer" title={tx.cardHash}>{short(tx.cardHash)}</a><span>{short(tx.from)} → {short(tx.to)}</span><span>Sequence {tx.sequence} · mined #{snapshot.blocks.find(b => b.transactions.some(item => !isNftTrade(item) && item.signature === tx.signature))?.height ?? '—'}</span><Check size={14} aria-label={`Mined transfer ${i + 1}`}/></div>)}</div> : <p className="muted">No mined transfers or trades.</p>}
@@ -206,12 +211,22 @@ export default function App() {
? <div key={`pending-trade-${tx.proposerSignature}`}><LoaderCircle size={16}/><span>{tx.offered.length} cards {short(tx.proposer)} ⇄ {tx.requested.length} cards {short(tx.counterparty)}</span><span className="pending-label">Atomic trade · awaiting mining</span></div>
: <div key={`${tx.cardHash}-${tx.sequence}`}><LoaderCircle size={16}/><span title={tx.cardHash}>{short(tx.cardHash)}</span><span>{short(tx.from)} → {short(tx.to)}</span><span className="pending-label">Awaiting mining</span></div>)}</div>
</section>}
<footer><a href="https://en.wikipedia.org" target="_blank" rel="noreferrer"><BookOpen size={16} /> Built on the shoulders of Wikipedia.</a><span>Knowledge, not financial promises.<span className="footer-star">✦</span></span><p>Independent experiment, not affiliated with Wikimedia. Mined article snapshots are selected from Wikipedia; attribution and image licenses appear on each card. Native NFTs are not ERC-721 tokens, fiat, or rights to article content. No promised monetary value.</p></footer>
<footer><a href="https://en.wikipedia.org" target="_blank" rel="noreferrer"><BookOpen size={16} /> Wikipedia pages. Collectible discoveries.</a><span>Mine. Collect. Trade.<span className="footer-star">✦</span></span><p>Independent project, not affiliated with Wikimedia or Monero. NFT rewards represent Wikipedia article snapshots, not ownership of Wikipedia pages or their copyright. Attribution and image licenses appear on each card. Current network: experimental testnet; cryptocurrency payments are simulated and TCG play is planned. No promised monetary value.</p></footer>
</main>
</div>
{toast && <div className="toast" role="status"><span><Check size={16} /></span>{toast}<button onClick={() => setToast('')} aria-label="Dismiss notification"><X size={14} /></button></div>}
{(dialog || selected) && <div className="modal-backdrop" onClick={e => { if (e.target === e.currentTarget) close(); }}><section className={`modal ${dialog === 'wallet' ? 'wallet-modal' : selected && !dialog ? 'card-modal' : ''}`} role="dialog" aria-modal="true" aria-label={dialog === 'wallet' ? 'Local testnet wallet' : dialog === 'transfer' ? 'Transfer native NFT' : dialog === 'guide' ? 'How Wikideck works' : 'Card details'}><button className="modal-close" aria-label="Close dialog" onClick={close} disabled={transferBusy}><X size={20} /></button>
{dialog === 'guide' ? <><span className="modal-icon"><Sparkles size={24} /></span><div className="section-eyebrow">THE TESTNET FIELD GUIDE</div><h2>A universe in every card.</h2><div className="guide-steps"><div><span>01</span><div><h3>Mine natively. Only by choice.</h3><p>No browser mining or automatic native execution. Build the trusted RandomX binary and run the miner yourself with an encrypted wallet backup and explicit --consent. One worker defaults to light mode (256 MiB cache plus overhead); --fast adds a ~2 GiB dataset. Mining uses CPU and energy, may warm your device, and is not guaranteed ASIC-proof. Ctrl+C stops it.</p></div></div><div><span>02</span><div><h3>A page, a snapshot. A sovereign chain.</h3><p>Each mining template asks Wikipedia for a random English article with a picture. A page can appear on multiple NFTs; each block commits its own article and mint-time statistics snapshot. Validators verify the snapshot's structure and commitments without contacting Wikimedia during consensus validation. Independent peers validate RandomX work, native NFT mints, and signed transactions, choosing the cumulative-work canonical chain. This testnet is unbootstrapped: there is no public mainnet, and an isolated node has no peer-backed network security.</p></div></div><div><span>03</span><div><h3>Keep your keys. Trade as a team.</h3><p>Your P-256 wallet is a local signing identity, not a server login. Download a password-encrypted backup and restore it locally to control your NFTs. Use Trade decks to review a public wallet’s confirmed holdings and prepare a signed multi-card offer. The other player signs before submission; all cards exchange together when the single trade transaction is mined. Offers do not reserve cards and can go stale. A pending transfer or trade does not change confirmed ownership. Mints require two subsequent blocks at inclusion height; reorganizations can change confirmations and ownership.</p></div></div></div><div className="info-box"><strong>Native NFTs, not content ownership.</strong><p>Token IDs identify native assets on this chain, not ERC-721 tokens on another network. RandomX proof hashes determine game rarity and stats; block IDs identify blocks. An NFT does not grant copyright or other intellectual-property rights to Wikipedia text or images. Follow their licenses and attribution. No fiat, financial return, or monetary value is promised.</p></div><button className="button dark full" onClick={() => { close(); goMine(); }}>Explore native mining <ArrowRight size={16} /></button></>
{dialog === 'guide' ? <>
<span className="modal-icon"><Sparkles size={24}/></span><div className="section-eyebrow">THE WIKIDECK FIELD GUIDE</div><h2>Mine. Collect. Make it yours.</h2>
<div className="guide-steps">
<div><span>01</span><div><h3>Mine a block. Earn a Wikipedia card.</h3><p>Wikideck is an NFT blockchain: each accepted block rewards its miner with a card based on a random illustrated Wikipedia page and its mint-time statistics. RandomX, also used by Monero, is designed for competitive mining on consumer CPUs. Wikideck is its own network, not part of Monero. Mining uses CPU, memory and energy; rewards depend on hardware and network difficulty.</p></div></div>
<div><span>02</span><div><h3>Build a collection. Trade your discoveries.</h3><p>Collect broadly or focus on a theme. Your encrypted local wallet controls your cards. Trade decks lets two players sign a multi-card swap that settles together when mined. Fixed-price sales and timed auctions live in the connected peer’s market. Transfers remain pending until mined, and chain reorganizations can change ownership and confirmations.</p></div></div>
<div><span>03</span><div><h3>Host a peer. Grow a community.</h3><p>A peer can host a general-purpose market or a community devoted to a collection. Listings and orders stay with that host, while card ownership follows the shared blockchain. Sales allocate a configurable hosting fee to the operator and a hardcoded 1% author fee. Prices are in USD with XMR cryptocurrency settlement planned; today payments, fees and payouts are simulations only.</p></div></div>
<div><span>04</span><div><h3>Custom trading card games — coming later.</h3><p>Playing TCGs with your cards and hosting custom TCGs on a peer are planned, not implemented. The open-source direction is to give operators freedom to adapt their market and future games; publishing the software license is still pending. Built-in collection curation and custom rules hosting are also future work. Shared consensus remains the same across compatible peers.</p></div></div>
</div>
<div className="info-box"><strong>Collectible cards, not ownership of Wikipedia.</strong><p>NFT rewards represent article snapshots, not rights to Wikipedia text or images. Follow each card’s attribution and licenses. This is an experimental testnet, not a launched production network; never send real XMR to its mock checkout. Current stats are not final TCG rules or a promise of financial value.</p></div>
<button className="button dark full" onClick={() => { close(); goMine(); }}>Explore RandomX mining <ArrowRight size={16}/></button>
</>
: dialog === 'wallet' ? <><span className="modal-icon"><WalletIcon size={24} /></span><div className="section-eyebrow">YOUR LOCAL TESTNET IDENTITY</div><h2>A wallet for your wonders.</h2><p className="muted">No server login. This recoverable wallet controls native NFTs through local signatures. It is separate from the original demo wallet.</p><label className="field-label">YOUR RECEIVE ADDRESS</label><div className="address-box">{wallet?.address ?? 'Wallet unavailable. Enable local browser storage and retry.'}</div><button className="button dark full" onClick={copyAddress} disabled={!wallet}><Copy size={16} /> Copy wallet address</button><div className="wallet-recovery"><WalletPanel wallet={wallet} onRestore={updateWallet} onNotice={setToast} /></div><p className="wallet-count"><Layers3 size={17} /> {owned.length} confirmed native NFTs in your collection</p></>
: dialog === 'transfer' ? <><span className="modal-icon"><ArrowUpRight size={24} /></span><div className="section-eyebrow">PASS ON THE CURIOSITY</div><h2>Send {selected?.article.title}.</h2><p className="muted">Use the recipient’s 64-character testnet wallet address on this same chain. Signing submits a pending transaction; it does not immediately transfer ownership.</p><label className="field-label">NFT TOKEN ID</label><div className="address-box">{selected?.block?.tokenId}</div><label className="field-label" htmlFor="recipient">RECIPIENT ADDRESS</label><input className="recipient-input" id="recipient" value={recipient} disabled={transferBusy} onChange={e => setRecipient(e.target.value)} placeholder="Paste a 64-character wallet address" autoComplete="off" maxLength={64} /><div className="info-box"><strong>Review before signing.</strong><p>Transfers take effect only when mined into the canonical chain. Once confirmed, there is no undo button or recovery for a wrong address. Mints require two subsequent blocks at inclusion height; only one pending transfer per NFT is allowed.</p></div>{(!rpcAvailable || transferDisabledReason) && <p className="transfer-error" role="alert">{!rpcAvailable ? 'RPC unavailable. Reconnect before signing.' : transferDisabledReason}</p>}{transferError && <p className="transfer-error" role="alert">{transferError}</p>}<button className="button dark full" disabled={transferBusy || !rpcAvailable || !!transferDisabledReason || !/^[a-fA-F0-9]{64}$/.test(recipient.trim()) || recipient.trim().toLowerCase() === wallet?.address} onClick={transfer}>{transferBusy ? <LoaderCircle className="spin" size={16} /> : <ArrowUpRight size={16} />}{transferBusy ? 'Signing & submitting…' : 'Sign & submit pending transfer'}</button></>
: selected && <><div className="detail-card"><Card card={selected} /></div><div className="detail-copy"><div className="section-eyebrow">{selected.block ? `MINTED IN BLOCK #${selected.block.height}` : 'EXAMPLE CARD · NOT OWNED'}</div><h2>{selected.article.title}</h2><p>{selected.article.extract}</p><div className="detail-facts"><span>Page ID<strong>#{selected.article.pageId}</strong></span><span>Rarity<strong>{rarity(selected.hash)}</strong></span><span>Content<strong>{selected.article.source === 'live' ? `Pinned revision ${selected.article.revisionId}` : 'Sample preview'}</strong></span></div>{selected.block && <><label className="field-label">NATIVE NFT TOKEN ID</label><div className="address-box">{selected.block.tokenId}</div><label className="field-label">BLOCK ID</label><div className="address-box">{selected.block.hash}</div><label className="field-label">RANDOMX PROOF HASH · GAME STATS</label><div className="address-box">{selected.hash}</div><p className="muted">{confirmations} canonical confirmations{!rpcAvailable ? ' · last known state' : ''}. {selectedBlock ? `Confirmed owner: ${selectedOwner}` : 'This mint is not on the loaded canonical chain.'}</p>{pending && <p className="pending-label">Transfer pending mining · confirmed owner unchanged.</p>}</>}<div className="detail-links"><a href={selected.article.url} target="_blank" rel="noreferrer">Read on Wikipedia <ExternalLink size={14} /></a><a href={imageCredit(selected.article)} target="_blank" rel="noreferrer">Image & license <ExternalLink size={14} /></a>{selected.block && <a href={`/api/nfts/${encodeURIComponent(selected.block.tokenId)}`} target="_blank" rel="noreferrer">NFT metadata <ExternalLink size={14} /></a>}</div><p className="attribution">{selected.article.source === 'live' ? 'Pinned text by Wikipedia contributors, CC BY-SA 4.0. Excerpt shortened for card display.' : 'Original fixed starter summary, linked to the real Wikipedia article; no live revision captured.'} Stats are game values, not encyclopedia facts. NFT ownership grants no rights to article text or images.</p>{selected.block && selectedOwner === wallet?.address && <>{transferDisabledReason && <p className="muted">{transferDisabledReason}</p>}<button className="button dark full" disabled={!rpcAvailable || !!transferDisabledReason} onClick={() => { setRecipient(''); setTransferError(''); setDialog('transfer'); }}>Transfer native NFT <ArrowUpRight size={16} /></button></>}</div></>}
+7 -6
View File
@@ -10,7 +10,7 @@ import './market.css';
type Props = { wallet?: Wallet; snapshot?: TestnetSnapshot; rpcAvailable: boolean; onNotice(message: string): void };
type Page = { listings: MarketListing[]; next: number | null; info: MarketInfo };
const short = (v: string) => `${v.slice(0, 8)}…${v.slice(-5)}`;
const feeDisclosure = (marketBps: number) => `Market fee: ${marketBps / 100}% plus a fixed 1% additional fee (${(marketBps + ADDITIONAL_MARKET_FEE.bps) / 100}% total), both deducted from seller proceeds. Additional fee recipient: ${ADDITIONAL_MARKET_FEE.address}. Payouts are simulated only.`;
const feeDisclosure = (marketBps: number) => `Hosting peer fee: ${marketBps / 100}% plus a hardcoded 1% project author fee (${(marketBps + ADDITIONAL_MARKET_FEE.bps) / 100}% total), both deducted from seller proceeds. Author fee recipient: ${ADDITIONAL_MARKET_FEE.address}. Payouts are simulated only.`;
async function api<T>(body?: unknown, offset = 0): Promise<T> {
const response = await fetch(body ? '/api/market/command' : `/api/market?offset=${offset}`, {
cache: 'no-store', signal: AbortSignal.timeout(15000),
@@ -124,10 +124,11 @@ export function Marketplace({ wallet, snapshot, rpcAvailable, onNotice }: Props)
};
const disabled = busy || !wallet || !rpcAvailable || !page;
return <section className="marketplace">
<header className="market-hero"><div><div className="section-eyebrow">THE COLLECTOR’S EXCHANGE</div><h1>A new home for<br/><span>extraordinary finds.</span></h1><p>Discover a fixed-price favorite or join a timed auction. Prices in USD, checkout in XMR. Your keys always sign card delivery.</p></div><div className="market-symbol"><ShoppingBag size={52}/><span>KNOWLEDGE CHANGES HANDS</span></div></header>
<header className="market-hero"><div><div className="section-eyebrow">THIS PEER’S COLLECTOR EXCHANGE</div><h1>Your next find.<br/><span>Your kind of market.</span></h1><p>Buy a fixed-price card or join a timed auction. Every peer can host its own market: a broad exchange or a community centered on a collection. Prices are in USD; cryptocurrency checkout is designed for Monero (XMR), currently simulated.</p></div><div className="market-symbol"><ShoppingBag size={52}/><span>PEER-HOSTED · COMMUNITY-DRIVEN</span></div></header>
<p className="muted">Sales support the hosting peer’s operator through a configurable fee and the project author through a hardcoded 1% fee. Both are deducted from seller proceeds. Collection-specific curation tools and peer-hosted custom TCGs are planned, not available yet.</p>
<div className="market-simulation" role="note"><ShieldNote/><div><strong>Mock payments only · never send real XMR</strong><p>Quotes, payment confirmations, refunds, fees, and automatic seller payouts are simulated. Card delivery is a real on-chain transfer. There is no payable Monero address. Listings are local to this marketplace node, not shared by peers.</p></div><span className="market-mode">SANDBOX</span></div>
<div className="market-toolbar"><div className="market-tabs" role="group" aria-label="Marketplace section">{(['browse','sell','orders'] as const).map(t=><button key={t} className={tab===t?'selected':''} onClick={()=>setTab(t)}>{t==='browse'?'Explore market':t==='sell'?'Sell a card':'Your activity'}</button>)}</div><button className="text-button" onClick={()=>{setError('');void refresh().catch(e=>setError(String(e.message)));}} disabled={busy||!rpcAvailable}><RefreshCw size={14}/>Refresh</button></div>
{page&&<><div className="market-meta"><span>Market fee <strong>{page.info.feeBps/100}% + 1% additional</strong> · {(page.info.feeBps+ADDITIONAL_MARKET_FEE.bps)/100}% total deducted from seller</span><span>Delivery <strong>{page.info.deliveryConfirmations} confirmations</strong> before mock payout</span><span>Simulation rate <strong>${page.info.mockUsdPerXmr}/XMR</strong> · not a live quote</span></div><p className="market-fee-recipient">Fixed 1% fee recipient · simulated payout only, not a checkout address: <span>{ADDITIONAL_MARKET_FEE.address}</span></p></>}
{page&&<><div className="market-meta"><span>Host <strong>{page.info.feeBps/100}%</strong> + author <strong>1%</strong> · {(page.info.feeBps+ADDITIONAL_MARKET_FEE.bps)/100}% total deducted from seller</span><span>Delivery <strong>{page.info.deliveryConfirmations} confirmations</strong> before mock payout</span><span>Simulation rate <strong>${page.info.mockUsdPerXmr}/XMR</strong> · not a live quote</span></div><p className="market-fee-recipient">Hardcoded project author fee recipient · simulated payout only, not a checkout address: <span>{ADDITIONAL_MARKET_FEE.address}</span></p></>}
{!rpcAvailable&&<p className="market-error" role="alert">Reconnect to the node before using the marketplace.</p>}
{!wallet&&<p className="muted">Open your local wallet to list cards, bid, and check out.</p>}
{error&&<p className="market-error" role="alert">{error}</p>}
@@ -138,7 +139,7 @@ export function Marketplace({ wallet, snapshot, rpcAvailable, onNotice }: Props)
const article=snapshot?.blocks.find(b=>b.tokenId===l.cardHash)?.article;
return <article className="market-card" key={l.id}><div className="market-art"><img src={l.image} alt="" loading="lazy"/><span>{l.kind==='auction'?<Gavel size={12}/>:<Tag size={12}/>} {l.kind==='auction'?'Timed auction':'Fixed price'}</span></div><div className="market-card-body"><small>BY {short(l.seller)} {mine?'· YOU':''}</small><h2>{l.title}</h2><div className="market-price"><strong>{formatUsd(salePrice)}</strong><span>{l.kind==='fixed'?'XMR at checkout':l.highestBid?'Highest bid':'Starting bid'}</span></div>{l.kind==='auction'&&<p className="market-countdown"><Clock3 size={13}/>{deadline(l.endsAt!,now)}{ended&&l.highestBid?` · winner ${short(l.highestBid.bidder)}`:''}</p>}
{l.status==='reserved'?<p className="market-reserved">Reserved · awaiting settlement</p>:mine?<button className="button outline full" disabled={!!disabled||!!l.highestBid} onClick={()=>prepare({type:'cancel',listingId:l.id},`Cancel the listing for ${l.title}?`)}>Cancel listing</button>:l.kind==='fixed'||ended?<button className="button dark full" disabled={!!disabled||(ended&&l.highestBid?.bidder!==wallet?.address)} onClick={()=>prepare({type:'buy',listingId:l.id,priceCents:salePrice,feeBps:l.feeBps},`Reserve ${l.title} for ${formatUsd(salePrice)} with simulated XMR checkout. ${feeDisclosure(l.feeBps)}. Complete mock payment within 30 minutes ${ended?'of auction closing':'of reservation'}.`)}><ShoppingBag size={14}/>{ended?'Winner checkout':'Buy now'}</button>:<div className="market-bid"><label className="sr-only" htmlFor={`bid-${l.id}`}>Your USD bid for {l.title}</label><input id={`bid-${l.id}`} inputMode="decimal" value={bids[l.id]??''} placeholder={formatUsd(l.highestBid?salePrice+1:salePrice)} onChange={e=>setBids(v=>({...v,[l.id]:e.target.value}))}/><button className="button dark" disabled={!!disabled} onClick={()=>{try{const priceCents=parseUsd(bids[l.id]??'');prepare({type:'bid',listingId:l.id,priceCents},`Bid ${formatUsd(priceCents)} on ${l.title}. If you win, check out within 30 minutes of closing. Bids are signed but not funded; all payment is simulated.`);}catch(e){setError(e instanceof Error?e.message:'Invalid bid');}}}><Gavel size={14}/>Bid</button></div>}
<p className="market-card-foot">{l.feeBps/100}% market + {(l.additionalFee?.bps ?? 0)/100}% additional seller fee · NFT {short(l.cardHash)}</p>{article&&<p className="market-card-foot"><a href={article.url} target="_blank" rel="noreferrer">Wikipedia</a> · <a href={imageCredit(article)} target="_blank" rel="noreferrer">Image & license</a> · <a href={`/api/nfts/${l.cardHash}`} target="_blank" rel="noreferrer">NFT metadata</a></p>}</div></article>;
<p className="market-card-foot">{l.feeBps/100}% host + {(l.additionalFee?.bps ?? 0)/100}% author · deducted from seller · NFT {short(l.cardHash)}</p>{article&&<p className="market-card-foot"><a href={article.url} target="_blank" rel="noreferrer">Wikipedia</a> · <a href={imageCredit(article)} target="_blank" rel="noreferrer">Image & license</a> · <a href={`/api/nfts/${l.cardHash}`} target="_blank" rel="noreferrer">NFT metadata</a></p>}</div></article>;
})}</div>{!page?.listings.length&&<div className="market-empty"><ShoppingBag size={30}/><h2>A discovery worth sharing?</h2><p>The market is waiting for its first listing. Offer a card from your confirmed collection.</p><button className="button dark" onClick={()=>setTab('sell')}>List your first card</button></div>}{page?.next!==null&&page?.next!==undefined&&<button className="button outline" onClick={loadMore}>Load more listings</button>}</>}
{tab==='sell'&&<section className="market-sell"><div><div className="section-eyebrow">MAKE ROOM FOR THE NEXT DISCOVERY</div><h2>Put a card on the market.</h2><p>Keep a set price open indefinitely, or let collectors bid until the clock runs out. You retain custody of the card and must return to sign delivery after mock payment.</p><p>A listing does not lock a card in consensus. Transfers elsewhere can invalidate it. No cancellation after an auction’s first bid; the winner gets 30 minutes to pay. Seller delivery deadline is 48 hours after mock payment.</p></div><form onSubmit={e=>{e.preventDefault();create();}}><label htmlFor="market-card">YOUR MATURE, UNLISTED CARD</label><select id="market-card" value={cardHash} onChange={e=>setCardHash(e.target.value)} required><option value="">Choose a card…</option>{owned.map(b=><option key={b.tokenId} value={b.tokenId}>{b.article.title} · {short(b.tokenId)}</option>)}</select><label htmlFor="market-kind">SALE FORMAT</label><select id="market-kind" value={kind} onChange={e=>setKind(e.target.value as typeof kind)}><option value="fixed">Fixed price · no expiry</option><option value="auction">Timed auction</option></select><label htmlFor="market-price">{kind==='fixed'?'PRICE':'STARTING BID'} · USD</label><input id="market-price" inputMode="decimal" value={price} onChange={e=>setPrice(e.target.value)} required/>{kind==='auction'&&<><label htmlFor="market-hours">AUCTION LENGTH · HOURS</label><input id="market-hours" type="number" min="0.016667" max="720" step="any" value={hours} onChange={e=>setHours(e.target.value)} required/></>}<p className="muted">Fees: {page?(page.info.feeBps/100)+'% market + 1% additional':'—'} · both subtracted from seller proceeds, never added to the buyer price.</p><button className="button dark" type="submit" disabled={!!disabled||!owned.length}><Tag size={15}/>Review listing</button></form></section>}
{tab==='orders'&&<section className="market-activity">
@@ -152,8 +153,8 @@ export function Marketplace({ wallet, snapshot, rpcAvailable, onNotice }: Props)
<dl>
<div><dt>Sale</dt><dd>{formatUsd(o.priceCents)}</dd></div>
<div><dt>Mock checkout</dt><dd>{o.invoice.amountXmr} XMR</dd></div>
<div><dt>Market fee ({o.feeBps/100}%)</dt><dd>{o.feeXmr} XMR</dd></div>
{o.additionalFee && <div><dt>Additional fee ({o.additionalFee.bps/100}%) · simulated</dt><dd>{o.additionalFee.amountXmr} XMR · {o.additionalFee.payout}<span className="market-fee-recipient">Recipient: {o.additionalFee.address}</span></dd></div>}
<div><dt>Hosting peer fee ({o.feeBps/100}%) · simulated</dt><dd>{o.feeXmr} XMR</dd></div>
{o.additionalFee && <div><dt>Project author fee ({o.additionalFee.bps/100}%) · simulated</dt><dd>{o.additionalFee.amountXmr} XMR · {o.additionalFee.payout}<span className="market-fee-recipient">Recipient: {o.additionalFee.address}</span></dd></div>}
<div><dt>Seller net · simulated</dt><dd>{o.sellerNetXmr} XMR</dd></div>
</dl>
<p className="muted">{o.status==='awaiting-payment'?`Pay by ${new Date(o.payBy).toLocaleString()}`:o.status==='paid'?`Seller signs delivery by ${new Date(o.deliverBy).toLocaleString()}`:`Delivery: ${o.deliveryConfirmations}/${o.requiredConfirmations} confirmations · payout: ${o.payout}`}</p>