import { useEffect, useRef, useState } from 'react'; import { ArrowDownLeft, ArrowLeftRight, ArrowRight, ArrowUpRight, BookOpen, Check, ChevronRight, CircleHelp, Copy, Cpu, Download, ExternalLink, Globe2, Layers3, LayoutDashboard, LoaderCircle, Pickaxe, Search, Shield, ShoppingBag, Sparkles, Wallet as WalletIcon, X } from 'lucide-react'; import { rarity } from '../shared/protocol.ts'; import { CONSENSUS, NETWORK, isNftTrade, nftTransferMessage, testnetOwnerOf, transactionCardIds, type TestnetBlock, type TestnetSnapshot } from '../shared/testnet.ts'; import { getWallet, sign, type Wallet } from './wallet.ts'; import { Card, samples, imageCredit, type CardData } from './Card.tsx'; import { WalletPanel } from './WalletPanel.tsx'; import { useModalFocus } from './useModalFocus.ts'; import { ThemePicker } from './ThemePicker.tsx'; import { TradeDesk } from './TradeDesk.tsx'; import { Marketplace } from './Marketplace.tsx'; import { IS_MAINNET, NETWORK_LABEL } from '../shared/network.ts'; import { StateClient } from '../shared/rpc-client.ts'; import './App.css'; import './testnet.css'; type View = 'Discover' | 'My collection' | 'Block explorer' | 'Trade decks' | 'Marketplace'; const short = (s: string) => s ? `${s.slice(0, 6)}…${s.slice(-4)}` : '—'; const asCard = (block: TestnetBlock): CardData => ({ article: block.article, hash: block.powHash, block }); const message = (error: unknown) => error instanceof Error ? error.message : 'Request failed. Please try again.'; async function request(path: string, body?: unknown, expectedStatus?: number): Promise { const res = await fetch(`/api/${path}`, { cache: 'no-store', signal: AbortSignal.timeout(15000), ...(body === undefined ? {} : { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }) }); if (!res.headers.get('content-type')?.includes('application/json')) throw new Error('The testnet RPC is unavailable. Start the node and web services.'); const data = await res.json(); if (!res.ok) throw new Error(typeof data.error === 'string' ? data.error : 'Request failed'); if (expectedStatus !== undefined && res.status !== expectedStatus) throw new Error('Unexpected testnet RPC response. Refresh before trying again.'); return data; } // Only canonical, mined transfers determine ownership and the next signature sequence. function transferProblem(state: TestnetSnapshot, card: CardData, address: string): string { const mint = card.block; const block = mint && state.blocks.find(b => b.tokenId === mint.tokenId && b.hash === mint.hash); if (!block) return 'This NFT is no longer on the canonical chain. Refresh your collection.'; if (testnetOwnerOf(state, block.tokenId) !== address) return 'This wallet is not the confirmed owner.'; if (state.blocks.length + 1 - block.height < CONSENSUS.coinbaseMaturity) return 'Immature NFT: transfers require two subsequent blocks at the next inclusion height.'; if (state.pendingTransfers.some(tx => transactionCardIds(tx).includes(block.tokenId))) return 'A transfer or trade involving this NFT is already pending mining.'; return ''; } export default function App() { const [view, setView] = useState('Discover'); const [wallet, setWallet] = useState(); const [snapshot, setSnapshot] = useState(); const [rpcAvailable, setRpcAvailable] = useState(false); const [error, setError] = useState(''); const [toast, setToast] = useState(''); const [dialog, setDialog] = useState<'guide' | 'wallet' | 'transfer' | null>(null); const [selected, setSelected] = useState(); const [query, setQuery] = useState(''); const [category, setCategory] = useState('All cards'); const [recipient, setRecipient] = useState(''); const [transferBusy, setTransferBusy] = useState(false); const [transferError, setTransferError] = useState(''); const transferInFlight = useRef(false); const refreshVersion = useRef(0); const minerPanel = useRef(null); const stateClient = useRef(new StateClient()); useModalFocus(Boolean(dialog || selected)); const refresh = async () => { const version = ++refreshVersion.current; try { const data = await stateClient.current.load(); if (data.network !== NETWORK || data.algorithm !== 'RandomX' || typeof data.chainId !== 'string' || !Array.isArray(data.blocks) || !Array.isArray(data.transfers) || !Array.isArray(data.pendingTransfers)) throw new Error('Incompatible node. This interface requires the Wikideck RandomX testnet API.'); if (version === refreshVersion.current) { setSnapshot(data); setRpcAvailable(true); setError(''); } return data; } catch (e) { if (version === refreshVersion.current) { setRpcAvailable(false); setError(message(e)); } throw e; } }; useEffect(() => { let active = true; getWallet().then(value => { if (active) setWallet(value); }).catch(e => { if (active) setError(message(e)); }); void refresh().catch(() => {}); const timer = setInterval(() => { void refresh().catch(() => {}); }, 8000); return () => { active = false; clearInterval(timer); refreshVersion.current++; }; }, []); useEffect(() => { if (toast) { const timer = setTimeout(() => setToast(''), 8000); return () => clearTimeout(timer); } }, [toast]); useEffect(() => { const escape = (event: KeyboardEvent) => { if (event.key === 'Escape' && !transferInFlight.current) { setDialog(null); setSelected(undefined); setTransferError(''); } }; document.addEventListener('keydown', escape); return () => document.removeEventListener('keydown', escape); }, []); const transfer = async () => { if (transferInFlight.current || !wallet || !selected?.block || !snapshot) return; transferInFlight.current = true; setTransferBusy(true); setTransferError(''); try { const state = await refresh(); if (state.chainId !== snapshot.chainId) throw new Error('The node chain ID changed. Refresh and review this NFT before signing.'); const problem = transferProblem(state, selected, wallet.address); if (problem) throw new Error(problem); const to = recipient.trim().toLowerCase(); if (!/^[a-f0-9]{64}$/.test(to) || to === wallet.address) throw new Error('Enter another wallet’s 64-character receive address.'); const tokenId = selected.block.tokenId; const t = { network: NETWORK, chainId: state.chainId, cardHash: tokenId, from: wallet.address, to, sequence: state.transfers.filter(tx => transactionCardIds(tx).includes(tokenId)).length }; const tx = { ...t, publicKey: wallet.publicKey, signature: await sign(wallet, nftTransferMessage(t)) }; await request('transfer', tx, 202); // A 202 is a mempool acknowledgement, NOT a change of confirmed ownership. refreshVersion.current++; setSnapshot(current => current && current.chainId === state.chainId ? { ...current, pendingTransfers: [...current.pendingTransfers.filter(p => !transactionCardIds(p).includes(tx.cardHash)), tx] } : current); setDialog(null); setRecipient(''); setToast('Transfer pending. You remain the confirmed owner until it is mined into the canonical chain.'); void refresh().catch(() => {}); } catch (e) { setTransferError(message(e)); } finally { transferInFlight.current = false; setTransferBusy(false); } }; const owned = snapshot?.blocks.filter(b => wallet && testnetOwnerOf(snapshot, b.tokenId) === wallet.address) ?? []; const visibleCards = view === 'My collection' ? owned.map(asCard) : snapshot?.blocks.length ? [...snapshot.blocks].reverse().slice(0, 12).map(asCard) : samples; const filtered = visibleCards.filter(c => (category === 'All cards' || c.article.category === category) && `${c.article.title} ${c.article.extract}`.toLowerCase().includes(query.toLowerCase())); const selectedMint = selected?.block; const selectedBlock = selectedMint && snapshot?.blocks.find(b => b.tokenId === selectedMint.tokenId && b.hash === selectedMint.hash); const confirmations = selectedBlock && snapshot ? snapshot.blocks.length - selectedBlock.height + 1 : 0; const selectedOwner = selectedBlock && snapshot ? testnetOwnerOf(snapshot, selectedBlock.tokenId) : undefined; const pending = !!selectedBlock && !!snapshot?.pendingTransfers.some(tx => transactionCardIds(tx).includes(selectedBlock.tokenId)); const transferDisabledReason = selected && wallet && snapshot ? transferProblem(snapshot, selected, wallet.address) : 'Wallet and node state are required.'; const peerLabel = !rpcAvailable ? 'Peers unknown' : snapshot?.peers ? `${snapshot.peers} peers` : 'Isolated node'; const nativeCommand = `npm run miner -- --wallet --consent --node ${window.location.origin}`; const close = () => { if (!transferInFlight.current) { setDialog(null); setSelected(undefined); setTransferError(''); } }; const openWallet = () => { setSelected(undefined); setDialog('wallet'); }; const goMine = () => { setView('Discover'); setTimeout(() => minerPanel.current?.scrollIntoView({ behavior: window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth', block: 'center' }), 50); }; const copyAddress = async () => { if (!wallet) return; try { await navigator.clipboard.writeText(wallet.address); setToast('Wallet address copied.'); } catch { setToast('Clipboard unavailable. Select and copy your receive address below.'); } }; const updateWallet = (restored: Wallet) => { setWallet(restored); setSelected(undefined); setRecipient(''); setTransferError(''); setToast('Wallet restored locally. Refreshing confirmed NFT ownership.'); void refresh().catch(() => {}); }; const retry = async () => { try { setWallet(await getWallet()); await refresh(); } catch (e) { setError(message(e)); } }; return
Explore {view}
RandomX {NETWORK_LABEL}{peerLabel}
RPC {rpcAvailable ? 'available' : 'unavailable'}{peerLabel}{IS_MAINNET?'Gated mainnet candidate · unaudited · mock payments only':'Unbootstrapped testnet · no public mainnet'}{snapshot && !rpcAvailable && Showing last known state}
{error &&
{error}
} {view === 'Discover' && <>
KNOWLEDGE, COLLECTED ON ITS OWN CHAIN.

Mine knowledge.
Collect the extraordinary.

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.

Independent testnet No browser mining
✳+
Preview cards · not owned
setSelected(samples[2])} />
setSelected(samples[0])} />
A world worth collecting
Native NFTs minted{snapshot?.blocks.length.toLocaleString() ?? '—'}canonical chain
Block height{snapshot ? `#${snapshot.blocks.at(-1)?.height ?? 0}` : '—'}current tip
Confirmed ownership{wallet && snapshot ? owned.length : '—'}in your wallet
Proof of work{snapshot?.algorithm ?? 'RandomX'}native CPU mining
EXPLICIT OPT-IN · OUTSIDE YOUR BROWSER

Native RandomX mining

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.

One native worker Independent validating peers
Light · default256 MiB cache + scratchpad and overhead.Fast · explicit --fast~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.
NEXT WIKIPEDIA ARTICLE{snapshot ? 'Chosen when the miner requests work' : 'Waiting for testnet RPC…'}

Each mining template requests a random English Wikipedia article with a Wikimedia picture. Pages may repeat; every minted card carries its own statistics snapshot.

  1. Build wikideck-randomx using the repository’s native build instructions.
  2. Save your encrypted backup and keep its password separate.
  3. Replace <backup.json> with your local file path (quote paths with spaces), then run:
{nativeCommand}

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.

} {view === 'Marketplace' ? : view === 'Trade decks' ? : view !== 'Block explorer' ?
{view === 'My collection' ? 'YOUR PERSONAL ENCYCLOPEDIA' : 'AN ENCYCLOPEDIA, REIMAGINED'}

{view === 'My collection' ? 'Your cabinet of curiosities' : 'A world of possible discoveries'}{visibleCards.length}

{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.'}

{view === 'Discover' && }
{['All cards', 'Nature', 'Space', 'Science', 'People', 'History', 'Culture'].map(c => )}
{filtered.length ?
{filtered.map(c =>
{ setSelected(c); setTransferError(''); }} />
{c.block ? `Block #${c.block.height}` : 'Example discovery'}{c.block && snapshot ? short(testnetOwnerOf(snapshot, c.block.tokenId) ?? '') : 'Not owned'}
{c.block && snapshot?.pendingTransfers.some(tx => transactionCardIds(tx).includes(c.block!.tokenId)) && Transfer or trade pending mining}
)}
:
Wikideck

{query || category !== 'All cards' ? 'No discoveries here. Yet.' : 'Your first discovery is waiting.'}

{query || category !== 'All cards' ? 'Try another search or explore a different category.' : 'Use the native miner or receive a mined transfer on this chain. Restore a backup to access an existing wallet.'}

}
:
FOLLOW THE CANONICAL CHAIN

Block explorer{snapshot?.blocks.length ?? 0}

Native NFT mints, RandomX proofs, and mined ownership transfers.

View full chain JSON

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.

Cumulative work
{snapshot?.cumulativeWork ?? '—'}
Peer count
{rpcAvailable ? snapshot?.peers ?? '—' : 'Unknown'} · {peerLabel}
Chain ID · pinned genesis
{snapshot?.chainId ?? '—'}
Network
{NETWORK}
Tip block ID
{snapshot?.tip ?? '—'}
Mint maturity
{snapshot?.maturity ?? CONSENSUS.coinbaseMaturity} subsequent blocks at inclusion height
{snapshot?.blocks.length ?
{[...snapshot.blocks].reverse().map(b => )}
Block / confirmationsDiscoveryBlock ID / NFT token IDConfirmed ownerRandomX proof / rarity
{snapshot.blocks.length - b.height + 1} confirmations{b.article.title}Wikipedia #{b.article.pageId}Block {short(b.hash)}NFT {short(b.tokenId)} {short(testnetOwnerOf(snapshot, b.tokenId) ?? '')}{short(b.powHash)}{rarity(b.powHash)}
:
Wikideck

A new chain. A blank page.

No canonical NFT blocks loaded. Native mining is an explicit, separate step.

}

Mined transfers and trades

{snapshot?.transfers.length ?
{snapshot.transfers.map((tx, i) => isNftTrade(tx) ?
{tx.offered.length} cards {short(tx.proposer)} ⇄ {tx.requested.length} cards {short(tx.counterparty)}Atomic trade · mined #{snapshot.blocks.find(b => b.transactions.some(item => isNftTrade(item) && item.proposerSignature === tx.proposerSignature))?.height ?? '—'}
:
{short(tx.cardHash)}{short(tx.from)} → {short(tx.to)}Sequence {tx.sequence} · mined #{snapshot.blocks.find(b => b.transactions.some(item => !isNftTrade(item) && item.signature === tx.signature))?.height ?? '—'}
)}
:

No mined transfers or trades.

}

Pending transfers and trades {snapshot?.pendingTransfers.length ?? 0}

Mempool only · not confirmed ownership. A trade bundle is included in full or not at all; pending transactions may be dropped or invalidated by a reorganization.

{snapshot?.pendingTransfers.map((tx, i) => isNftTrade(tx) ?
{tx.offered.length} cards {short(tx.proposer)} ⇄ {tx.requested.length} cards {short(tx.counterparty)}Atomic trade · awaiting mining
:
{short(tx.cardHash)}{short(tx.from)} → {short(tx.to)}Awaiting mining
)}
}
Built on the shoulders of Wikipedia.Knowledge, not financial promises.✦

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.

{toast &&
{toast}
} {(dialog || selected) &&
{ if (e.target === e.currentTarget) close(); }}>
{dialog === 'guide' ? <>
THE TESTNET FIELD GUIDE

A universe in every card.

01

Mine natively. Only by choice.

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.

02

A page, a snapshot. A sovereign chain.

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.

03

Keep your keys. Trade as a team.

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.

Native NFTs, not content ownership.

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.

: dialog === 'wallet' ? <>
YOUR LOCAL TESTNET IDENTITY

A wallet for your wonders.

No server login. This recoverable wallet controls native NFTs through local signatures. It is separate from the original demo wallet.

{wallet?.address ?? 'Wallet unavailable. Enable local browser storage and retry.'}

{owned.length} confirmed native NFTs in your collection

: dialog === 'transfer' ? <>
PASS ON THE CURIOSITY

Send {selected?.article.title}.

Use the recipient’s 64-character testnet wallet address on this same chain. Signing submits a pending transaction; it does not immediately transfer ownership.

{selected?.block?.tokenId}
setRecipient(e.target.value)} placeholder="Paste a 64-character wallet address" autoComplete="off" maxLength={64} />
Review before signing.

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.

{(!rpcAvailable || transferDisabledReason) &&

{!rpcAvailable ? 'RPC unavailable. Reconnect before signing.' : transferDisabledReason}

}{transferError &&

{transferError}

} : selected && <>
{selected.block ? `MINTED IN BLOCK #${selected.block.height}` : 'EXAMPLE CARD · NOT OWNED'}

{selected.article.title}

{selected.article.extract}

Page ID#{selected.article.pageId}Rarity{rarity(selected.hash)}Content{selected.article.source === 'live' ? `Pinned revision ${selected.article.revisionId}` : 'Sample preview'}
{selected.block && <>
{selected.block.tokenId}
{selected.block.hash}
{selected.hash}

{confirmations} canonical confirmations{!rpcAvailable ? ' · last known state' : ''}. {selectedBlock ? `Confirmed owner: ${selectedOwner}` : 'This mint is not on the loaded canonical chain.'}

{pending &&

Transfer pending mining · confirmed owner unchanged.

}}

{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.

{selected.block && selectedOwner === wallet?.address && <>{transferDisabledReason &&

{transferDisabledReason}

}}
}
}
; }