Removed markets

This commit is contained in:
2026-10-28 17:09:58 -04:00
parent 216a419c0b
commit eea2deb1fb
11 changed files with 122 additions and 105 deletions
+26 -33
View File
@@ -1,42 +1,44 @@
# Wikideck
**Production roadmap:** see the [production delivery plan](docs/production-plan.md) for XMR-native pricing, custodial-market implementation, fixed-price-first launch scope and release gates. New v3 listings use exact native-XMR prices; historical v2 USD records keep their original terms. All payments remain simulated. The [custodial design and implementation status](docs/custodial-market-design.md) distinguish the sandbox ledger from future live custody.
Wikideck is an NFT blockchain where mining a block rewards the miner with a collectible card based on a Wikipedia page. Build collections, trade cards with other collectors, or host a peer with its own marketplace. Trading card games (TCGs), including custom games hosted by peers, are planned for a later release.
Wikideck is an experimental blockchain game about **mining, collecting, and trading illustrated Wikipedia cards**. Mine a block with the native RandomX CPU miner to earn a card, build a collection in a local wallet, and trade cards directly with another player. The default app has no marketplace, prices, or checkout.
Mining uses **RandomX, the proof-of-work algorithm also used by Monero**, designed to keep general-purpose consumer CPUs competitive. Wikideck is an independent blockchain, not part of Monero or its mining/security network. Hardware, total network hashpower and difficulty determine results; no particular mining reward rate or profitability is guaranteed.
**This is a local test network, not a public mainnet or an investment product.** No public bootnodes or hosted network are provided. Test NFTs have no promised value. Do not use funds or valuable assets. This project is not affiliated with Wikipedia or the Wikimedia Foundation.
## Collect, trade, and build your own community
## Mine, collect, and trade cards
- **Mine Wikipedia cards.** Each accepted block mints a native NFT for the miner from a random illustrated English Wikipedia article. The card records the article snapshot and mint-time statistics. The same page can appear on more than one distinct card. The reward is a card representing a page—not ownership of the page, its copyright, or a payment to Wikipedia.
- **Build collections.** Assemble a broad cabinet of discoveries or focus on a subject, series or theme. Browse public decks and exchange up to eight cards on each side in one atomic trade.
- **Buy and sell in peer-hosted markets.** Running a peer lets you host a market for the shared chain's cards. Your community can be general-purpose or focus on a particular collection. Listings, auctions and orders belong to that market host; card ownership and mined transfers are shared through the blockchain.
- **Support the host and project author.** Sales allocate a configurable hosting fee to the peer's market operator (2.5% by default) and a separate hardcoded **1% author fee** to the project's fixed XMR address. Both are deducted from seller proceeds; the default total is 3.5%. In the intended cryptocurrency settlement model the host retains its fee, while the author allocation is paid separately. Today all payment, fee and payout accounting is simulated.
- **Cryptocurrency checkout.** New market prices are quoted directly in **Monero (XMR)** atomic units. The current testnet checkout is mock-only: no real cryptocurrency is accepted or sent.
- **Play TCGs — planned.** The roadmap is to use these same cards in trading card games and let peers host custom TCG experiences. There is no playable game engine, match system, deck legality or custom rules hosting yet. Current card stats are not a promise of future game balance.
- **Build collections.** Browse chain discoveries or focus on a subject, series, or theme. Cards are controlled by your encrypted local wallet.
- **Trade directly.** Look up another public wallet and exchange up to eight cards on each side in one signed, atomic trade. Offers are shared between the two players; the app does not list cards for sale or process payments.
- **Gameplay is not included yet.** The current release supports mining, collecting, transfers, and direct trades. Playable card games are future work.
## Peer ownership and customization
## Optional legacy market modules
**Understand the market before trading:** [Payment flow and buyer/seller protections](docs/market-transparency.md) explains who controls cards and funds, both fees, deadlines, refund/reorg limits, privacy and host accountability. Current payments are mock-only; seller delivery still transfers the actual card. No guaranteed refund, insurance or built-in arbitration is provided.
Market and custody code remains in the repository for separate experiments, but it is **not part of the default card-trading experience**. The server does not open a market journal, expose market APIs, or run its reconciliation worker unless `WIKIDECK_MARKET_RUNTIME_ENABLED=1` is explicitly set. Enabling it is an operator decision; market orders are local to the node, checkout is simulated, and no real XMR is accepted or sent. See the [market implementation notes](docs/marketplace.md) and [security boundaries](docs/market-transparency.md) before enabling it. Existing market data is not deleted when the feature is off, but disabling an existing market pauses order resolution; review outstanding orders before switching modes.
The project's open-source direction is to give peer operators substantial freedom to adapt their own market: presentation, community focus, curation and future game rules. A collection-focused community can run the current general listing interface today; built-in collection filters, moderation/admission tooling and custom TCG hosting are future work, not existing operator controls. Operators can already configure their hosting fee and run their own local marketplace journal.
Market customization does not let one peer rewrite shared NFT ownership or unilaterally change consensus for other peers. Compatible peers must follow the same network rules. The author fee is hardcoded in the distributed implementation; it is not a hosting configuration option. Any source modifications and redistribution must respect the eventual project license and third-party licenses.
**Licensing status:** this checkout does not yet contain a project LICENSE file. Source availability alone does not grant open-source modification/redistribution rights. Publishing an appropriate license is still required before describing those freedoms as legally granted. Wikipedia text/images and dependencies retain their separate licenses; a Wikideck software license would not replace them.
**Licensing status:** this checkout does not yet contain a project LICENSE file. Source availability alone does not grant open-source modification/redistribution rights. Wikipedia text/images and dependencies retain their separate licenses.
## What's available now
| Available in the testnet | Planned / not enabled |
| Available in the testnet | Not included in the default experience |
| --- | --- |
| RandomX mining and illustrated Wikipedia NFT rewards | Reviewed production network launch |
| Encrypted local wallets, collections and atomic card trades | Playable TCGs and peer-hosted custom games |
| Per-peer fixed-price listings and timed auctions | Built-in collection-specific curation/admission controls |
| Mock native-XMR checkout and host/author fee accounting | Real cryptocurrency settlement and production custody operations |
| RandomX mining and illustrated Wikipedia card rewards | Marketplace listings, auctions, and checkout |
| Encrypted local wallets, collections, transfers, and atomic card trades | Real cryptocurrency payments or custody |
| Local single-node or optional three-node testnet | Playable card games and production network launch |
## Run the three-node testnet (Docker)
## Quick start: one local node (Docker)
Install Docker Desktop with Linux containers and Docker Compose v2. From the repository root:
```sh
docker compose up --build -d
docker compose ps
```
Open **http://127.0.0.1:3001**. The first run builds the node and native RandomX engine. The node does not mine automatically. Open the wallet panel to create a local wallet and encrypted backup, then follow [Mine Wikipedia cards with RandomX](#mine-wikipedia-cards-with-randomx) to run the miner. The default stack leaves marketplace startup and APIs disabled. Stop the node with `docker compose down`; the ledger volume remains. **Do not add `-v` unless you intend to delete that data.**
## Optional: three-node testnet (Docker)
Install Docker Desktop with Linux containers and Docker Compose v2. From the repository root, start three connected nodes:
@@ -46,7 +48,7 @@ docker compose -f compose.testnet.yaml ps
docker compose -f compose.testnet.yaml logs -f node-a
```
Open **http://127.0.0.1:3101**. The other nodes are at **http://127.0.0.1:3102** and **http://127.0.0.1:3103**. Each node validates blocks independently and stores its ledger in a separate Docker volume. Mining does not start automatically. A healthy node is not necessarily connected to its peers; check the peer status in the interface or request `/api/peers`. Nodes need outbound access to Wikipedia when generating mining templates and capturing mint-time statistics. If that data is unavailable, the node returns an error and mining can be retried.
Open **http://127.0.0.1:3101**. The other nodes are at **http://127.0.0.1:3102** and **http://127.0.0.1:3103**. Each node validates blocks independently and stores its ledger in a separate Docker volume. Marketplace runtime is disabled by default; mining does not start automatically. A healthy node is not necessarily connected to its peers; check the peer status in the interface or request `/api/peers`. Nodes need outbound access to Wikipedia when generating mining templates and capturing mint-time statistics. If that data is unavailable, the node returns an error and mining can be retried.
Stop the nodes with:
@@ -60,15 +62,7 @@ For an existing host install, keep the old data directory intact and set `WIKIDE
## Run on your host
### Optional custody accounting preview
**Optional market hosting:** set `WIKIDECK_MARKET_ENABLED=0` to suppress public listings/new trading, or `WIKIDECK_MARKET_TRADING_ENABLED=0` for a browsable paused market. Configure hosting-fee payouts with `WIKIDECK_MARKET_HOST_FEE_ADDRESS` (defaults to the author wallet); the fixed 1% author fee remains separate. Existing orders/balance resolution stay accessible. See [host configuration](docs/market-host-configuration.md) for strict values, signed recipient snapshots and safe container recreation.
For a **persistent PostgreSQL-backed local stack**, use [the deployment guide](docs/persistent-custody-sandbox.md) and [compose.custody.yaml](compose.custody.yaml), or the VS Code **Start persistent custody sandbox** task. It starts PostgreSQL plus Wikideck at port 3422, with generated scoped credentials, verified database TLS and explicit migrations. Existing single/three-node commands are unchanged. This uses separate volumes and does not migrate existing SQLite custody or enable real payments.
The marketplace's **Custody preview** tab is separate from existing mock checkout. Explicit `WIKIDECK_CUSTODY_MODE=sandbox` on the server enables fictitious test balances, signed fixed-price reservations and withdrawal holds on testnet only. Default is `off`; live modes and mainnet sandbox fail closed. Use a disposable data directory and configure `PUBLIC_ORIGIN` to the exact browser origin. No Monero addresses, wallet RPC or real withdrawals are available. [PostgreSQL financial storage](docs/postgres-financial-storage.md) is selectable with `WIKIDECK_CUSTODY_STORAGE=postgres`, explicit schema/role setup and a secret-file connection configuration; existing SQLite custody requires a stopped-node import, never automatic fallback.
Reservations stop at `awaiting-lock` because reviewed NFT sale-lock consensus rules are not implemented. They can be cancelled while their outbox intent is blocked; they do not deliver cards. Local SQLite custody is included by stopped-node backup/restore when present; PostgreSQL-bound nodes require coordinated database/node backups and the SQLite-only backup command refuses them. Real treasury observation/settlement, signing controls and production review remain future work. See [the implementation record](docs/custodial-market-design.md).
The legacy market can be explicitly enabled with `WIKIDECK_MARKET_RUNTIME_ENABLED=1`; do not enable it for a node that has unresolved orders without reviewing its local journal first. When disabled, the market journal is left untouched, but the node does not reconcile it or expose market APIs. The separate custody and Monero experiments require dedicated configurations and are not needed to mine or trade cards. Their deployment instructions remain in the `docs/` directory, but are outside the supported quick-start path.
Requirements: Node.js 24, npm, Git, CMake 3.24+, and a C/C++17 toolchain. The first native-engine configuration downloads the pinned RandomX source over HTTPS. On Windows, install a compatible toolchain (for example, MinGW-w64 and make) and use the matching CMake generator. Do not change generators in an existing `native/build` directory; use a fresh build directory when switching toolchains.
@@ -115,7 +109,6 @@ Light mode needs a 256 MiB cache plus scratchpad and process overhead. `--fast`
- **My collection** shows NFTs owned by the active local wallet on the connected node's canonical chain.
- **Block explorer** distinguishes block IDs, NFT token IDs, and RandomX proof hashes. A submitted transfer is pending until a block confirms it. Reorganizations can change canonical ownership and confirmations.
- **Trade decks** lets you look up another public wallet by receive address, select up to eight cards on each side, prepare a signed offer, and accept another player's offer. Offers are shared as JSON for the other player to review; they are not submitted until accepted. The two parties sign the same card list, and validators apply the whole bundle atomically or reject it entirely. Offers are not reservations: before acceptance the app re-checks ownership, maturity, sequences, and pending activity; stale offers must be recreated.
- **Marketplace** is the connected peer's market: new fixed-price listings and timed auctions use native XMR amounts; historical USD listings remain read-only. Checkout, fees and payouts are simulated. Sales allocate a configurable fee to the hosting operator (default 2.5%) and a hardcoded 1% author fee (3.5% total by default). Sellers still sign real card delivery after the mock payment step. **Never send real XMR: no payable checkout address or live processor is enabled.** Listings/orders are local to the selected market host, not gossiped; listing does not lock a card on-chain. See [peer markets, fees and payment status](docs/marketplace.md).
- Each minted card shows current Wikipedia statistics alongside its immutable at-mint snapshot: pageviews, article size, language links, revision, and derived scores. Current values may change; at-mint values are committed to the block. Missing pageview data stays unavailable, not zero.
- The appearance selector offers Catppuccin Mocha, Macchiato, Frappé, and Latte themes.
+1
View File
@@ -89,6 +89,7 @@ services:
WIKIDECK_MARKET_TRADING_ENABLED: "${WIKIDECK_MARKET_TRADING_ENABLED:-1}"
WIKIDECK_MARKET_HOST_FEE_ADDRESS: "${WIKIDECK_MARKET_HOST_FEE_ADDRESS:-44xHcCdFhW4b951yBHpkTPdLSJ85qTCnFQ8N7m96TtqqRL3dbhzbEH4fe2SJ3a6ESHcNyzCYH5nYJ9ActR1JtkeA7QYGbQv}"
WIKIDECK_CUSTODY_MODE: sandbox
WIKIDECK_MARKET_RUNTIME_ENABLED: "${WIKIDECK_MARKET_RUNTIME_ENABLED:-1}"
WIKIDECK_CUSTODY_STORAGE: postgres
WIKIDECK_CUSTODY_PG_URL_FILE: /secrets/runtime/url
WIKIDECK_CUSTODY_PG_CA_FILE: /secrets/runtime/ca.crt
+1
View File
@@ -79,6 +79,7 @@ services:
WIKIDECK_MARKET_TRADING_ENABLED: "${WIKIDECK_MARKET_TRADING_ENABLED:-1}"
WIKIDECK_MARKET_HOST_FEE_ADDRESS: "${WIKIDECK_MARKET_HOST_FEE_ADDRESS:-44xHcCdFhW4b951yBHpkTPdLSJ85qTCnFQ8N7m96TtqqRL3dbhzbEH4fe2SJ3a6ESHcNyzCYH5nYJ9ActR1JtkeA7QYGbQv}"
WIKIDECK_CUSTODY_MODE: sandbox
WIKIDECK_MARKET_RUNTIME_ENABLED: "${WIKIDECK_MARKET_RUNTIME_ENABLED:-1}"
WIKIDECK_CUSTODY_STORAGE: postgres
WIKIDECK_CUSTODY_PG_URL_FILE: /secrets/runtime/url
WIKIDECK_CUSTODY_PG_CA_FILE: /secrets/runtime/ca.crt
+1
View File
@@ -12,6 +12,7 @@ services:
- "127.0.0.1:${WIKIDECK_MAINNET_PORT:-3201}:3001"
environment:
WIKIDECK_NETWORK: mainnet
WIKIDECK_MARKET_RUNTIME_ENABLED: "${WIKIDECK_MARKET_RUNTIME_ENABLED:-1}"
WIKIDECK_PAYMENT_MODE: mock
WIKIDECK_MARKET_ENABLED: "${WIKIDECK_MARKET_ENABLED:-1}"
WIKIDECK_MARKET_TRADING_ENABLED: "${WIKIDECK_MARKET_TRADING_ENABLED:-1}"
+6 -5
View File
@@ -1,7 +1,8 @@
# Three independent validators, separate volumes, no miner auto-starts.
x-market-environment: &market-environment
# Three independent validators, separate volumes, no miner or market auto-starts.
x-node-environment: &node-environment
WIKIDECK_NETWORK: testnet
WIKIDECK_PAYMENT_MODE: mock
WIKIDECK_MARKET_RUNTIME_ENABLED: "${WIKIDECK_MARKET_RUNTIME_ENABLED:-0}"
WIKIDECK_MARKET_ENABLED: "${WIKIDECK_MARKET_ENABLED:-1}"
WIKIDECK_MARKET_TRADING_ENABLED: "${WIKIDECK_MARKET_TRADING_ENABLED:-1}"
WIKIDECK_MARKET_HOST_FEE_ADDRESS: "${WIKIDECK_MARKET_HOST_FEE_ADDRESS:-44xHcCdFhW4b951yBHpkTPdLSJ85qTCnFQ8N7m96TtqqRL3dbhzbEH4fe2SJ3a6ESHcNyzCYH5nYJ9ActR1JtkeA7QYGbQv}"
@@ -13,7 +14,7 @@ services:
build: { context: ., target: runtime }
ports: ["127.0.0.1:3101:3001"]
environment:
<<: *market-environment
<<: *node-environment
PUBLIC_ORIGIN: http://127.0.0.1:3101
WIKIDECK_PEERS: http://node-b:3001,http://node-c:3001
WIKIDECK_ALLOW_PRIVATE_PEERS: "1"
@@ -32,7 +33,7 @@ services:
build: { context: ., target: runtime }
ports: ["127.0.0.1:3102:3001"]
environment:
<<: *market-environment
<<: *node-environment
PUBLIC_ORIGIN: http://127.0.0.1:3102
WIKIDECK_PEERS: http://node-a:3001,http://node-c:3001
WIKIDECK_ALLOW_PRIVATE_PEERS: "1"
@@ -51,7 +52,7 @@ services:
build: { context: ., target: runtime }
ports: ["127.0.0.1:3103:3001"]
environment:
<<: *market-environment
<<: *node-environment
PUBLIC_ORIGIN: http://127.0.0.1:3103
WIKIDECK_PEERS: http://node-a:3001,http://node-b:3001
WIKIDECK_ALLOW_PRIVATE_PEERS: "1"
+1
View File
@@ -12,6 +12,7 @@ services:
WIKIDECK_PEERS: "${WIKIDECK_PEERS:-}"
WIKIDECK_ALLOW_PRIVATE_PEERS: "${WIKIDECK_ALLOW_PRIVATE_PEERS:-0}"
WIKIDECK_PAYMENT_MODE: mock
WIKIDECK_MARKET_RUNTIME_ENABLED: "${WIKIDECK_MARKET_RUNTIME_ENABLED:-0}"
WIKIDECK_MARKET_ENABLED: "${WIKIDECK_MARKET_ENABLED:-1}"
WIKIDECK_MARKET_TRADING_ENABLED: "${WIKIDECK_MARKET_TRADING_ENABLED:-1}"
WIKIDECK_MARKET_HOST_FEE_ADDRESS: "${WIKIDECK_MARKET_HOST_FEE_ADDRESS:-44xHcCdFhW4b951yBHpkTPdLSJ85qTCnFQ8N7m96TtqqRL3dbhzbEH4fe2SJ3a6ESHcNyzCYH5nYJ9ActR1JtkeA7QYGbQv}"
+5 -4
View File
@@ -1,9 +1,10 @@
# Optional market hosting and host fee wallet
These are **server runtime** environment variables, forwarded by the single-node, three-node, gated mainnet and persistent custody Compose files. They do not enable real payments.
These are **server runtime** environment variables, not browser settings. The single-node and three-node Compose profiles default the market runtime to off; the gated mainnet and dedicated custody profiles opt in. They do not enable real payments.
| Variable | Default | Effect |
| --- | --- | --- |
| `WIKIDECK_MARKET_RUNTIME_ENABLED` | `0` | `1` opens the local market journal, exposes market APIs, and starts reconciliation. `0` leaves market data untouched, does not reconcile it, and returns 404 for market APIs. Resolve existing orders before disabling a market with unresolved activity. |
| `WIKIDECK_MARKET_ENABLED` | `1` | `0` removes public listings and rejects new listings, bids, purchases, custody reservations and fictitious funding. Node validation, mining, peer sync and free transfers/trades continue unchanged. |
| `WIKIDECK_MARKET_TRADING_ENABLED` | `1` | `0` pauses new trading/funding but keeps public listings browsable. Useful for maintenance/read-only operation. |
| `WIKIDECK_MARKET_HOST_FEE_ADDRESS` | Fixed project author XMR address | Public Monero recipient for the **hosting commission**, independently of the fixed additional author commission. |
@@ -16,13 +17,13 @@ Default wallet:
44xHcCdFhW4b951yBHpkTPdLSJ85qTCnFQ8N7m96TtqqRL3dbhzbEH4fe2SJ3a6ESHcNyzCYH5nYJ9ActR1JtkeA7QYGbQv
Set the variables in the shell/Compose environment or a local Compose environment file, then recreate the intended app container. They are not browser build variables. Merely restarting a container does not replace its environment. Existing Compose defaults preserve current hosting behavior. In the three-node configuration shared values apply to all nodes; per-service environment overrides allow different hosts/fees. Do not point independent markets at a shared financial schema.
Set supported variables in the shell/Compose environment or a local Compose environment file, then recreate the intended app container. They are not browser build variables. Merely restarting a container does not replace its environment. The quick-start single-node and three-node profiles deliberately leave the market runtime off; explicitly enable it only for an operator-managed market. In the three-node configuration shared values apply to all nodes; per-service environment overrides allow different hosts/fees. Do not point independent markets at a shared financial schema.
## Existing obligations remain available
Disabling market hosting is **not a funds freeze or deletion**. Signed private activity, existing-order mock payment/delivery/refund, listing retirement/cancellation, custody cancellation and withdrawal-hold requests remain available under their existing checks. Reconciliation continues and data is retained. PostgreSQL remains required if that node already has PostgreSQL-backed liabilities; the switch is not a shortcut to abandoning custody responsibilities. Existing valid command receipts remain replayable without creating another mutation.
Turning off the market runtime is **not a funds freeze or deletion**. The market journal is preserved, but APIs and reconciliation are unavailable until the runtime is enabled again; this can pause existing-order resolution. Do not disable or stop an active market with unresolved orders or custody liabilities. PostgreSQL remains required if that node has PostgreSQL-backed liabilities; the switch is not a shortcut to abandoning custody responsibilities. The separate `WIKIDECK_MARKET_ENABLED=0` policy is different: it pauses new activity while retaining the market runtime and its existing resolution controls.
A peer that never wants custody can leave `WIKIDECK_CUSTODY_MODE=off` and use the ordinary single/three-node stack with `WIKIDECK_MARKET_ENABLED=0`; it does not need the dedicated PostgreSQL stack. Do not turn custody off merely to hide pre-existing liabilities; first resolve them under a reviewed shutdown procedure. The marketplace UI retains an activity/resolution surface and displays the disabled/paused policy rather than hiding recovery controls.
A peer that never wants custody can leave `WIKIDECK_CUSTODY_MODE=off`; mining, peer sync, and free card transfers/trades do not require the market runtime or dedicated PostgreSQL stack.
## Fee recipient and signature compatibility
+50 -34
View File
@@ -16,6 +16,7 @@ import { wikipediaEnrichment } from '../enrichment.ts';
import { snapshotStats } from '../../shared/enrichment.ts';
import { wikipediaArticles } from '../wiki.ts';
import { Market, marketConfig } from '../market/store.ts';
import { paymentMode } from '../market/payments.ts';
import { marketCommandSchema } from '../../shared/market.ts';
import { marketCommandV3Schema } from '../../shared/market-v3.ts';
import { custodyCommandSchema } from '../../shared/custody.ts';
@@ -40,9 +41,18 @@ import { trustedProxies, operationsPort, startOperations } from '../operations.t
const root = fileURLToPath(new URL('../../', import.meta.url));
assertLaunchAllowed();
const paymentConfig = marketConfig(); // Fail before opening node resources if live payments are requested.
await validateMarketPolicy(paymentConfig); // Reject invalid recipient checksum/network before opening node resources.
const marketRuntimeSetting = process.env.WIKIDECK_MARKET_RUNTIME_ENABLED ?? '0';
if (marketRuntimeSetting !== '0' && marketRuntimeSetting !== '1') {
throw new Error('WIKIDECK_MARKET_RUNTIME_ENABLED must be exactly 0 or 1');
}
const marketRuntimeEnabled = marketRuntimeSetting === '1';
const custodyConfig = custodyMode(); // Explicit opt-in sandbox only. Unknown/live modes fail before opening resources.
paymentMode(); // Keep real-payment modes fail-closed even when the optional market runtime is off.
let paymentConfig: ReturnType<typeof marketConfig> | undefined;
if (marketRuntimeEnabled || custodyConfig === 'sandbox') {
paymentConfig = marketConfig();
await validateMarketPolicy(paymentConfig); // Reject invalid recipient checksum/network before opening node resources.
}
const canonicalObserverMode = process.env.WIKIDECK_CUSTODY_CANONICAL_REHEARSAL;
if (canonicalObserverMode !== undefined && canonicalObserverMode !== '0' && canonicalObserverMode !== '1') {
throw new Error('WIKIDECK_CUSTODY_CANONICAL_REHEARSAL must be exactly 0 or 1');
@@ -86,8 +96,8 @@ let peers: PeerNetwork;
try { peers = new PeerNetwork({ ledger, urls:(process.env.WIKIDECK_PEERS ?? '').split(',').map(s=>s.trim()).filter(Boolean),
allowPrivate:process.env.WIKIDECK_ALLOW_PRIVATE_PEERS === '1' }); }
catch(e) {ledger.close();engine.close();await lock.close();await unlink(lockPath);throw e;}
let market: Market;
try { market = await Market.open(dataDir, ledger, paymentConfig, Date.now, { worker: true }); }
let market: Market | undefined;
try { if (marketRuntimeEnabled) market = await Market.open(dataDir, ledger, paymentConfig!, Date.now, { worker: true }); }
catch(e) {peers.close();ledger.close();engine.close();await lock.close();await unlink(lockPath);throw e;}
let custody: CustodyMarket | PostgresCustody | undefined;
let custodyObserver: CanonicalObserver | undefined;
@@ -97,7 +107,7 @@ let moneroDepositContext: MainnetEnrollmentContext | undefined;
let moneroWithdrawalPool: Pool | undefined;
let moneroWithdrawalCustodianId: string | undefined;
try {
if (custodyConfig === 'sandbox') custody = await openCustodyStorage(dataDir, { ...paymentConfig,
if (custodyConfig === 'sandbox') custody = await openCustodyStorage(dataDir, { ...paymentConfig!,
card: (hash, owner, sequence) => {
const nft = ledger.nft(hash);
if (!nft || nft.owner !== owner || nft.confirmations < CONSENSUS.coinbaseMaturity || nft.sequence !== sequence ||
@@ -147,7 +157,7 @@ try {
await verifyMainnetCustodyIdentity(moneroWithdrawalPool, moneroWithdrawalCustodianId);
await moneroWithdrawalPool.query('SELECT 1 FROM wk_custody.monero_withdrawal_intents LIMIT 0');
}
} catch(e) {await moneroWithdrawalPool?.end();await moneroDepositPool?.end();await custodyObserver?.close();await custody?.close();await market.close();peers.close();ledger.close();engine.close();await lock.close();await unlink(lockPath);throw e;}
} catch(e) {await moneroWithdrawalPool?.end();await moneroDepositPool?.end();await custodyObserver?.close();await custody?.close();await market?.close();peers.close();ledger.close();engine.close();await lock.close();await unlink(lockPath);throw e;}
const app = express();
app.disable('x-powered-by');
if(proxyTrust.length)app.set('trust proxy',proxyTrust);
@@ -181,7 +191,23 @@ app.get('/api/canonical',(req,res)=>{
app.get('/api/network',(_req,res)=>res.json(launchManifest()));
app.get('/api/chain',(_req,res)=>res.json(ledger.legacySnapshot(limits.legacyBlocks)));
app.get('/api/peers',(_req,res)=>res.json({peers:peers.status()}));
app.get('/api/market', (req,res)=>res.json(market.browse(Number(req.query.offset ?? 0))));
if (market) {
app.get('/api/market', (req,res)=>res.json(market!.browse(Number(req.query.offset ?? 0))));
app.post('/api/market/command',rateLimit({windowMs:60000,limit:60,standardHeaders:'draft-8',legacyHeaders:false}),async(req,res)=>{
const current = marketCommandV3Schema.safeParse(req.body);
const command = current.success ? current.data : marketCommandSchema.parse(req.body);
try {
const result = await market!.execute(command);
res.json(result);
if(command.action.type==='deliver')void peers.broadcastTransfer(command.action.transfer).catch(()=>{});
} catch(error) {
if(error instanceof RpcError)throw error;
// Marketplace storage/payment internals must never leak through the generic node error handler.
if(error instanceof Error && !/SQLITE|constraint|database/i.test(error.message))res.status(400).json({error:error.message.slice(0,200)});
else res.status(409).json({error:'Marketplace state conflict. Refresh and retry.'});
}
});
}
app.get('/api/custody', async (req,res)=> {
if (!custody) { res.json({ enabled: false }); return; }
try { res.json(await custody.browse(Number(req.query.offset ?? 0))); }
@@ -252,20 +278,6 @@ if (withdrawalRequestMode === '1' && moneroWithdrawalPool && moneroWithdrawalCus
}
});
}
app.post('/api/market/command',rateLimit({windowMs:60000,limit:60,standardHeaders:'draft-8',legacyHeaders:false}),async(req,res)=>{
const current = marketCommandV3Schema.safeParse(req.body);
const command = current.success ? current.data : marketCommandSchema.parse(req.body);
try {
const result = await market.execute(command);
res.json(result);
if(command.action.type==='deliver')void peers.broadcastTransfer(command.action.transfer).catch(()=>{});
} catch(error) {
if(error instanceof RpcError)throw error;
// Marketplace storage/payment internals must never leak through the generic node error handler.
if(error instanceof Error && !/SQLITE|constraint|database/i.test(error.message))res.status(400).json({error:error.message.slice(0,200)});
else res.status(409).json({error:'Marketplace state conflict. Refresh and retry.'});
}
});
app.get('/api/decks/:address', rateLimit({windowMs:60000,limit:60,standardHeaders:'draft-8',legacyHeaders:false}), (req,res)=>{
if(typeof req.params.address!=='string'||!/^[a-f0-9]{64}$/.test(req.params.address)){
res.status(400).json({error:'Invalid wallet address'});return;
@@ -359,7 +371,7 @@ const server=app.listen(port,host,()=>{
const operations=startOperations(opsPort,()=>({network:NETWORK,paymentMode:'mock',
ledger:ledger.diagnostics(),native:engine.diagnostics(),
peers:{configured:peers.status().length,connected:peers.status().filter(p=>p.connected).length},
rpc:{api:apiBudget.status(),p2p:peerBudget.status()},market:market.diagnostics(),
rpc:{api:apiBudget.status(),p2p:peerBudget.status()},market:market?.diagnostics() ?? { enabled:false },
canonicalObserver:custodyObserver?.status(),
custody: custody && 'diagnostics' in custody ? custody.diagnostics() : { backend: custody ? 'sqlite-sandbox' : 'off' }}),resolve(dataDir,PROFILE));
server.maxConnections=256;
@@ -368,16 +380,16 @@ server.requestTimeout=15000;
server.keepAliveTimeout=5000;
server.setTimeout(30000,socket=>socket.destroy());
// Advance simulated payout/expiry state even when no browser is open. No real funds are sent.
const marketTimer=setInterval(()=>{try {market.browse();}catch{console.error('Marketplace reconciliation failed; operator review required.');}},15000);
marketTimer.unref();
let stopping=false;
let catchupPaused=false;
let catchupImmediate:NodeJS.Immediate|undefined;
let marketTimer:NodeJS.Timeout|undefined;
let marketCatchupTimer:NodeJS.Timeout|undefined;
// Drain durable catch-up without page-load dependence. Each pass is a smaller synchronous
// SQLite slice, then setImmediate yields so HTTP and peer work can run between passes.
const advanceMarketCatchup=()=>{
catchupImmediate=undefined;
if(stopping||catchupPaused||market.isReconciliationWorkerBusy()||!market.hasReconciliationBacklog())return;
if(!market||stopping||catchupPaused||market.isReconciliationWorkerBusy()||!market.hasReconciliationBacklog())return;
if(market.isReconciliationWorkerFailed()){
catchupPaused=true;console.error('Marketplace reconciliation worker failed; operator review required.');return;
}
@@ -387,17 +399,21 @@ const advanceMarketCatchup=()=>{
catchupImmediate=setImmediate(advanceMarketCatchup);catchupImmediate.unref();
}
};
const marketCatchupTimer=setInterval(()=>{
if(!stopping&&!catchupPaused&&!catchupImmediate&&market.hasReconciliationBacklog()){
catchupImmediate=setImmediate(advanceMarketCatchup);catchupImmediate.unref();
}
},250);
marketCatchupTimer.unref();
if (market) {
marketTimer=setInterval(()=>{try {market!.browse();}catch{console.error('Marketplace reconciliation failed; operator review required.');}},15000);
marketTimer.unref();
marketCatchupTimer=setInterval(()=>{
if(!stopping&&!catchupPaused&&!catchupImmediate&&market!.hasReconciliationBacklog()){
catchupImmediate=setImmediate(advanceMarketCatchup);catchupImmediate.unref();
}
},250);
marketCatchupTimer.unref();
}
for(const signal of ['SIGTERM','SIGINT']as const)process.once(signal,()=>{
if(stopping)return;stopping=true;peers.close();operations.close();clearInterval(marketTimer);clearInterval(marketCatchupTimer);
if(stopping)return;stopping=true;peers.close();operations.close();if(marketTimer)clearInterval(marketTimer);if(marketCatchupTimer)clearInterval(marketCatchupTimer);
if(catchupImmediate)clearImmediate(catchupImmediate);
const observerDrain=custodyObserver?.close()??Promise.resolve();
const timeout=setTimeout(()=>process.exit(1),25000);timeout.unref();
server.close(async()=>{await observerDrain;await moneroWithdrawalPool?.end();await moneroDepositPool?.end();await custody?.close();await market.close();ledger.close();engine.close();await lock.close();await unlink(lockPath).catch(()=>{});clearTimeout(timeout);process.exit(0);});
server.close(async()=>{await observerDrain;await moneroWithdrawalPool?.end();await moneroDepositPool?.end();await custody?.close();await market?.close();ledger.close();engine.close();await lock.close();await unlink(lockPath).catch(()=>{});clearTimeout(timeout);process.exit(0);});
});
server.on('error',async()=>{stopping=true;clearInterval(marketTimer);clearInterval(marketCatchupTimer);if(catchupImmediate)clearImmediate(catchupImmediate);operations.close();peers.close();await custodyObserver?.close();await moneroWithdrawalPool?.end();await moneroDepositPool?.end();await custody?.close();await market.close();ledger.close();engine.close();await lock.close();await unlink(lockPath).catch(()=>{});process.exit(1);});
server.on('error',async()=>{stopping=true;if(marketTimer)clearInterval(marketTimer);if(marketCatchupTimer)clearInterval(marketCatchupTimer);if(catchupImmediate)clearImmediate(catchupImmediate);operations.close();peers.close();await custodyObserver?.close();await moneroWithdrawalPool?.end();await moneroDepositPool?.end();await custody?.close();await market?.close();ledger.close();engine.close();await lock.close();await unlink(lockPath).catch(()=>{});process.exit(1);});
+14 -16
View File
@@ -1,7 +1,7 @@
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';
Pickaxe, Search, Shield, 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';
@@ -11,13 +11,12 @@ 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, IS_PILOT, 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';
type View = 'Discover' | 'My collection' | 'Block explorer' | 'Trade cards';
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.';
@@ -162,8 +161,8 @@ export default function App() {
<aside className="sidebar">
<a href="#" className="brand" onClick={e => { e.preventDefault(); setView('Discover'); }}><span className="brand-mark" aria-hidden="true">W<span>✦</span></span>wikideck<span className="brand-dot">.</span></a>
<div className="sidebar-label">YOUR NEXT DISCOVERY</div>
<nav aria-label="Main navigation">{(['Discover', 'My collection', 'Block explorer', 'Trade decks', 'Marketplace'] as View[]).map((item, i) => {
const Icon = [LayoutDashboard, Layers3, Globe2, ArrowLeftRight, ShoppingBag][i];
<nav aria-label="Main navigation">{(['Discover', 'My collection', 'Block explorer', 'Trade cards'] as View[]).map((item, i) => {
const Icon = [LayoutDashboard, Layers3, Globe2, ArrowLeftRight][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>Mine together.<br />Trade your way.</h3><p>Discover illustrated Wikipedia cards, build a deck around what you love, and find your collector community.</p><button onClick={() => setDialog('guide')}>How it works <ArrowUpRight size={15} /></button></div>
@@ -177,7 +176,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 /> MINE · BUILD A DECK · TRADE WITH YOUR PEOPLE</span><h1>Mine with your CPU.<br />Build a deck from <span>the world.</span></h1><p>Turn random illustrated Wikipedia discoveries into collectible cards. Use the same RandomX algorithm as Monero, grow a collection around what you love, then trade with other collectors. Community markets are here; card games are on the way.</p><div className="hero-actions"><button className="button dark" onClick={goMine}><Pickaxe size={17} /> Explore 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} /> CPU-friendly RandomX</span><span><Check size={13} /> Collector-run markets</span></div></div>
<div className="hero-copy"><span className="eyebrow"><span /> MINE · COLLECT · TRADE CARDS</span><h1>Mine with your CPU.<br />Collect <span>the world.</span></h1><p>Turn illustrated Wikipedia discoveries into collectible cards. Mine with RandomX, build a collection around what you love, and trade cards directly with other collectors.</p><div className="hero-actions"><button className="button dark" onClick={goMine}><Pickaxe size={17} /> Explore 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} /> CPU-friendly RandomX</span><span><Check size={13} /> Direct card-for-card trades</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">
@@ -190,13 +189,13 @@ export default function App() {
<div className="mining-description"><div className="section-eyebrow"><span className="idle-dot" /> RANDOMX · DESIGNED FOR GENERAL-PURPOSE CPUS</div><h2>Mining should be for more people.</h2><p>Wikideck and Monero use RandomX, an algorithm designed to resist specialized ASICs and keep general-purpose CPUs competitive. That can make mining more accessible—not make every machine equally powerful or promise rewards. Each accepted Wikideck block earns its miner a distinct illustrated Wikipedia card. Wikideck is its own independent network.</p><div className="mining-meta"><span><Cpu size={13} /> Mine with a CPU</span><span><Shield size={13} /> Independently validating peers</span></div><details className="mining-details"><summary>Hardware, rewards &amp; network details</summary><div className="native-memory"><strong>Light · default</strong><span>256 MiB cache plus scratchpad and process overhead.</span><strong>Fast · --fast</strong><span>About 2 GiB dataset plus cache; budget over 2.3 GiB per process. Mining uses CPU, memory, electricity and time. ASIC resistance is a design goal, not a guarantee; results depend on hardware and network difficulty. Wikideck does not share Monero’s network, hashpower or security.</span></div></details></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">The miner selects a random illustrated English Wikipedia article. The same article can appear again; each card keeps its own mint-time statistics.</p><details className="mining-details"><summary>Want to run a miner? Setup &amp; safety details</summary><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}. The command connects to this node; use HTTPS for remote nodes. Password entry stays local—never put it in the command. It defaults to one worker and at most one block. Ctrl+C stops the native miner; closing this page does not.</p></details><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 · DECK-BUILD · TRADE</div><h2>Make every deck your own.</h2><p>Chase a theme, collect the unexpected, or put together a multi-card swap. List a favorite for a fixed price or let collectors bid in an auction.</p><button className="text-button" onClick={()=>setView('Trade decks')}>Meet other collectors <ArrowRight size={14}/></button></article>
<article><Globe2 size={22}/><div className="section-eyebrow">A MARKET FOR YOUR COMMUNITY</div><h2>Collectors set the scene.</h2><p>Each peer can run its own market, with listings local to that host. New listings use XMR prices, but checkout is simulated: no real XMR is accepted or sent. Host and author fees are shown before you sign.</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">NEXT UP · GAMEPLAY IS IN DEVELOPMENT</div><h2>Your cards, ready to play.</h2><p>Trading-card games and community-made rules are the bigger idea: build decks with a point of view, then play your way. The game engine and custom game hosting are planned, not playable today.</p><button className="text-button" onClick={()=>setDialog('guide')}>See what’s here today <ArrowRight size={14}/></button></article>
<section className="project-pillars" aria-label="Mine, collect, and trade cards">
<article><Pickaxe size={22}/><div className="section-eyebrow">MINE · EARN A CARD</div><h2>Discover the next card.</h2><p>Run the native RandomX miner to compete for blocks. Each accepted block rewards its miner with a distinct illustrated Wikipedia card.</p><button className="text-button" onClick={goMine}>Mining setup <ArrowRight size={14}/></button></article>
<article><Layers3 size={22}/><div className="section-eyebrow">COLLECT · BUILD YOUR DECK</div><h2>Make every collection your own.</h2><p>Browse discoveries on the chain, keep cards in your local wallet, and follow the subjects that interest you.</p><button className="text-button" onClick={()=>setView('My collection')}>Open my collection <ArrowRight size={14}/></button></article>
<article><ArrowLeftRight size={22}/><div className="section-eyebrow">TRADE · PLAYER TO PLAYER</div><h2>Swap cards directly.</h2><p>Prepare a signed offer for another collector and exchange card bundles atomically when the trade is mined. No prices or checkout.</p><button className="text-button" onClick={()=>setView('Trade cards')}>Trade cards <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">
{view === 'Trade cards' ? <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>
<div className="filter-bar"><div className="category-tabs">{['All cards', 'Nature', 'Space', 'Science', 'People', 'History', 'Culture'].map(c => <button key={c} className={category === c ? 'selected' : ''} onClick={() => setCategory(c)}>{c}</button>)}</div><label className="search-input"><Search size={15} /><input aria-label="Search cards" placeholder="Find a discovery…" value={query} onChange={e => setQuery(e.target.value)} /></label></div>
{filtered.length ? <div className="card-grid">{filtered.map(c => <div className="card-wrap" key={c.block?.tokenId ?? c.hash}><Card card={c} onClick={() => { setSelected(c); setTransferError(''); }} /><div className="card-list-caption"><span>{c.block ? `Block #${c.block.height}` : 'Example discovery'}</span><span className="caption-dot" />{c.block && snapshot ? short(testnetOwnerOf(snapshot, c.block.tokenId) ?? '') : 'Not owned'}</div>{c.block && snapshot?.pendingTransfers.some(tx => transactionCardIds(tx).includes(c.block!.tokenId)) && <span className="pending-label">Transfer or trade pending mining</span>}</div>)}</div> : <div className="empty-state"><img className="empty-state-logo" src="/original-logo.svg" alt="Wikideck" /><h3>{query || category !== 'All cards' ? 'No discoveries here. Yet.' : 'Your first discovery is waiting.'}</h3><p>{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.'}</p><button className="button dark" onClick={goMine}><Pickaxe size={16} /> Explore native mining</button></div>}
@@ -211,18 +210,17 @@ 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} /> 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>
<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 with 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 WIKIDECK FIELD GUIDE</div><h2>Mine. Build a deck. Find your people.</h2>
<span className="modal-icon"><Sparkles size={24}/></span><div className="section-eyebrow">THE WIKIDECK FIELD GUIDE</div><h2>Mine. Collect. Trade cards.</h2>
<div className="guide-steps">
<div><span>01</span><div><h3>Mine with a CPU. Discover a card.</h3><p>RandomX—the algorithm used by Wikideck and Monero—is designed to make specialized ASICs less advantageous and general-purpose CPUs more competitive. Mine a block to earn a one-of-a-kind card featuring a random illustrated Wikipedia article.</p><details><summary>Mining, rewards &amp; network facts</summary><p>ASIC resistance is a design goal, not a guarantee of equal performance. Results depend on hardware and network difficulty; there is no promised reward rate or profit. Wikideck is an independent network and does not share Monero’s hashpower or security. Mining uses CPU, memory, electricity and time.</p></details></div></div>
<div><span>02</span><div><h3>Build a deck that feels like yours.</h3><p>Collect across the encyclopedia or follow a theme. Your encrypted local wallet controls your cards. Trade multi-card bundles with another player, then explore fixed-price listings and timed auctions on the connected peer’s market.</p><details><summary>How ownership and trades work</summary><p>Both players sign the same card list; the whole swap is applied together when mined or rejected. Transfers remain pending until confirmed, and a chain reorganization can change confirmed ownership. Looking up a public wallet address reveals its confirmed holdings.</p></details></div></div>
<div><span>03</span><div><h3>Trade locally. Grow a collector community.</h3><p>Every peer can host its own market, so communities can create a place to trade around their interests. New listings are priced in XMR, but all checkout, fees and payouts are simulated today. Never send real XMR.</p><details><summary>Market fees &amp; what “simulated” means</summary><p>Listings and orders stay with the market host; NFT ownership follows the shared blockchain. The operator sets a hosting fee and the project author fee is 1%; both are deducted from simulated seller proceeds. Card delivery is a real on-chain transfer after the mock payment step. No payable Monero address or live payment processor is enabled.</p></details></div></div>
<div><span>04</span><div><h3>And then, play.</h3><p>Trading-card games are the fun we’re building toward: deck-building, collecting, and community-made ways to play with your cards.</p><details><summary>What is available now?</summary><p>Playable TCGs, match systems, deck rules and peer-hosted custom games are not implemented yet. Today’s card stats are collectible snapshots, not final game balance or a promise of financial value. This is an experimental testnet, not a public production network.</p></details></div></div>
<div><span>02</span><div><h3>Keep your collection in your wallet.</h3><p>Your encrypted local wallet controls your cards. Restore a backup to use the same identity in another browser profile. Confirmed ownership follows the canonical chain.</p><details><summary>Wallet and ownership basics</summary><p>Transfers remain pending until confirmed, and a chain reorganization can change confirmed ownership. Looking up a public wallet address reveals its confirmed holdings. Keep your encrypted backup and password separately.</p></details></div></div>
<div><span>03</span><div><h3>Trade cards directly with another collector.</h3><p>Both players sign the same card list; the complete swap is applied together when mined or rejected. Offers are shared between players and are not marketplace listings or sales.</p><details><summary>What happens when I accept?</summary><p>The app rechecks ownership, maturity, sequences, and pending activity before submitting. A trade stays pending until mined; re-check the recipient and cards before signing.</p></details></div></div>
</div>
<div className="info-box"><strong>Collectible snapshots—not ownership of Wikipedia.</strong><p>Cards do not grant rights to Wikipedia text or images. Check each card’s attribution and licenses. This is an experimental testnet; cards have no promised monetary value.</p></div>
<button className="button dark full" onClick={() => { close(); goMine(); }}>Explore RandomX mining <ArrowRight size={16}/></button>
+6 -1
View File
@@ -82,7 +82,7 @@ test('native RandomX HTTP: three blocks, mined transfer, peer gossip/sync and du
};
t.signal.addEventListener('abort', abort, { once: true });
async function start(dataDir: string, peer?: string): Promise<RunningNode> {
async function start(dataDir: string, peer?: string, marketRuntimeEnabled = true): Promise<RunningNode> {
t.signal.throwIfAborted();
const child = spawn(process.execPath, ['--import', wikipediaMock, '--experimental-strip-types', 'server/index.ts'], {
cwd: root, windowsHide: true, shell: false,
@@ -90,6 +90,7 @@ test('native RandomX HTTP: three blocks, mined transfer, peer gossip/sync and du
WIKIDECK_DATA_DIR: dataDir, RANDOMX_BINARY: binary,
WIKIDECK_PEERS: peer ?? '', WIKIDECK_ALLOW_PRIVATE_PEERS: peer ? '1' : '0',
PUBLIC_ORIGIN: 'http://127.0.0.1:3001', WIKIDECK_PAYMENT_MODE: 'mock',
WIKIDECK_MARKET_RUNTIME_ENABLED: marketRuntimeEnabled ? '1' : '0',
WIKIDECK_MARKET_FEE_BPS: '250', WIKIDECK_MARKET_CONFIRMATIONS: '2', WIKIDECK_CUSTODY_MODE: peer ? 'off' : 'sandbox',
// This integration burst now includes signed custody reads/replay probes; admission limits have dedicated tests.
WIKIDECK_RPC_WRITES_PER_SECOND: '50' },
@@ -218,6 +219,10 @@ test('native RandomX HTTP: three blocks, mined transfer, peer gossip/sync and du
engine = new RandomX({ binary }); // Always the actual reference RandomX engine, never a test double.
const alice = wallet(), bob = wallet();
assert.notEqual(alice.owner, bob.owner);
const marketDisabledNode = await start(firstDir, undefined, false);
await json(marketDisabledNode.base, '/api/summary');
await request(marketDisabledNode.base, '/api/market', 404);
await stop(marketDisabledNode);
let first = await start(firstDir);
await assert.rejects(start(firstDir), /Data directory locked/,
'a second server process must fail closed rather than run another marketplace worker on the same data directory');
+11 -12
View File
@@ -2,29 +2,28 @@ import test from 'node:test';
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
test('project overview distinguishes mining rewards, peer markets, planned TCGs and current payment status',async()=>{
test('project overview focuses on mining and player-to-player card trades',async()=>{
const readme=await readFile(new URL('../README.md',import.meta.url),'utf8');
assert.match(readme,/mining a block rewards the miner with a collectible card based on a Wikipedia page/);
assert.match(readme,/algorithm also used by Monero/);
assert.match(readme,/consumer CPUs competitive/);
assert.match(readme,/custom games hosted by peers.*planned/);
assert.match(readme,/configurable hosting fee/);
assert.match(readme,/hardcoded \*\*1% author fee\*\*/);
assert.match(readme,/Checkout, fees and payouts are simulated/);
assert.match(readme,/new fixed-price listings and timed auctions use native XMR amounts/);
assert.match(readme,/trade cards directly with another player/);
assert.match(readme,/not part of the default card-trading experience/);
assert.match(readme,/WIKIDECK_MARKET_RUNTIME_ENABLED=1/);
assert.doesNotMatch(readme,/## Peer ownership and customization/);
assert.match(readme,/does not yet contain a project LICENSE file/);
assert.doesNotMatch(readme,/## Mine only by explicit choice/);
for(const name of ['../compose.yaml','../compose.testnet.yaml'])
assert.match(await readFile(new URL(name,import.meta.url),'utf8'),/WIKIDECK_MARKET_RUNTIME_ENABLED:.*:-0/);
});
test('homepage sells the collector vision while disclosing mining and market limits',async()=>{
test('homepage presents mining, collections, and direct card trading without marketplace UI',async()=>{
const app=await readFile(new URL('../src/App.tsx',import.meta.url),'utf8');
assert.match(app,/Mine with your CPU/);
assert.match(app,/Build a deck that feels like yours/);
assert.match(app,/Direct card-for-card trades/);
assert.match(app,/designed to resist specialized ASICs/);
assert.match(app,/ASIC resistance is a design goal, not a guarantee/);
assert.match(app,/New listings are priced in XMR, but all checkout, fees and payouts are simulated today/);
assert.match(app,/Playable TCGs, match systems, deck rules and peer-hosted custom games are not implemented yet/);
assert.match(app,/Trade cards directly with another collector/);
assert.doesNotMatch(app,/Marketplace|Custody preview|fixed-price listings/);
assert.match(app,/<details className="mining-details"><summary>Want to run a miner\? Setup &amp; safety details/);
assert.doesNotMatch(app,/Prices are in USD with XMR cryptocurrency settlement planned/);
assert.match(app,/--consent/); // Documentation-only change: runnable miner invocation keeps its existing required flag.
});
test('market transparency explains real controls without promising escrow, refunds or live payments',async()=>{