Real XMR pilot
This commit is contained in:
@@ -115,7 +115,7 @@ 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: fixed-price listings, timed auctions, USD prices and mock XMR checkout. Sales allocate a configurable fee to the hosting operator (default 2.5%) and a hardcoded 1% author fee (3.5% total by default). Buyers simulate payment, sellers sign real card delivery, and simulated seller-net/author-fee payouts happen after canonical delivery confirmations. **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).
|
||||
- **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.
|
||||
|
||||
|
||||
Executable
+119
@@ -0,0 +1,119 @@
|
||||
# Private mock-only pilot profile. Start with a distinct Compose project name;
|
||||
# this stack intentionally contains no Monero daemon, Wallet-RPC, or observer.
|
||||
services:
|
||||
provision:
|
||||
image: wikideck:pilot-provision
|
||||
build:
|
||||
context: .
|
||||
dockerfile: deploy/postgres/Dockerfile
|
||||
volumes:
|
||||
- pg-admin:/secrets/admin
|
||||
- pg-owner:/secrets/owner
|
||||
- pg-runtime:/secrets/runtime
|
||||
- pg-backup:/secrets/backup
|
||||
- pg-tls:/secrets/tls
|
||||
network_mode: none
|
||||
read_only: true
|
||||
tmpfs: ["/tmp:mode=1777,size=16m"]
|
||||
restart: "no"
|
||||
database:
|
||||
image: wikideck:postgres-pilot
|
||||
build:
|
||||
context: .
|
||||
dockerfile: deploy/postgres/database.Dockerfile
|
||||
depends_on:
|
||||
provision:
|
||||
condition: service_completed_successfully
|
||||
environment:
|
||||
POSTGRES_DB: wikideck_custody
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD_FILE: /secrets/admin/password
|
||||
POSTGRES_INITDB_ARGS: --auth-host=scram-sha-256
|
||||
command: [postgres, -c, ssl=on, -c, ssl_min_protocol_version=TLSv1.2,
|
||||
-c, ssl_cert_file=/tls/server.crt, -c, ssl_key_file=/tls/server.key,
|
||||
-c, hba_file=/etc/wikideck/pg_hba.conf, -c, log_statement=none,
|
||||
-c, log_min_error_statement=panic, -c, log_parameter_max_length_on_error=0]
|
||||
volumes:
|
||||
- pg-data:/var/lib/postgresql/data
|
||||
- pg-admin:/secrets/admin:ro
|
||||
- pg-tls:/tls:ro
|
||||
networks: [financial]
|
||||
healthcheck:
|
||||
test: [CMD-SHELL, "pg_isready -U postgres -d wikideck_custody"]
|
||||
interval: 3s
|
||||
timeout: 3s
|
||||
retries: 30
|
||||
restart: unless-stopped
|
||||
shm_size: 128m
|
||||
stop_grace_period: 30s
|
||||
migrate:
|
||||
image: wikideck:private-pilot
|
||||
build:
|
||||
context: .
|
||||
target: runtime
|
||||
args:
|
||||
WIKIDECK_NETWORK: pilot
|
||||
depends_on:
|
||||
database:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
WIKIDECK_NETWORK: pilot
|
||||
WIKIDECK_LOCAL_PG_BOOTSTRAP: sandbox-only
|
||||
command: [node, --experimental-strip-types, scripts/custody-deployment-init.ts]
|
||||
volumes:
|
||||
- pg-admin:/secrets/admin:ro
|
||||
- pg-owner:/secrets/owner:ro
|
||||
- pg-runtime:/secrets/runtime:ro
|
||||
- pg-backup:/secrets/backup:ro
|
||||
networks: [financial]
|
||||
read_only: true
|
||||
cap_drop: [ALL]
|
||||
security_opt: [no-new-privileges:true]
|
||||
restart: "no"
|
||||
wikideck:
|
||||
image: wikideck:private-pilot
|
||||
build:
|
||||
context: .
|
||||
target: runtime
|
||||
args:
|
||||
WIKIDECK_NETWORK: pilot
|
||||
depends_on:
|
||||
migrate:
|
||||
condition: service_completed_successfully
|
||||
ports: ["127.0.0.1:${WIKIDECK_PILOT_PORT:-3423}:3001"]
|
||||
environment:
|
||||
WIKIDECK_NETWORK: pilot
|
||||
WIKIDECK_PILOT_MODE: private-mock-only
|
||||
WIKIDECK_PAYMENT_MODE: mock
|
||||
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}"
|
||||
WIKIDECK_CUSTODY_MODE: sandbox
|
||||
WIKIDECK_CUSTODY_STORAGE: postgres
|
||||
WIKIDECK_CUSTODY_PG_URL_FILE: /secrets/runtime/url
|
||||
WIKIDECK_CUSTODY_PG_CA_FILE: /secrets/runtime/ca.crt
|
||||
PUBLIC_ORIGIN: "http://127.0.0.1:${WIKIDECK_PILOT_PORT:-3423}"
|
||||
volumes:
|
||||
- custody-node:/app/data
|
||||
- pg-runtime:/secrets/runtime:ro
|
||||
networks: [financial, web]
|
||||
init: true
|
||||
restart: unless-stopped
|
||||
read_only: true
|
||||
tmpfs: ["/tmp:mode=1777,size=32m,noexec,nosuid"]
|
||||
cap_drop: [ALL]
|
||||
security_opt: [no-new-privileges:true]
|
||||
pids_limit: 128
|
||||
mem_limit: 1g
|
||||
stop_grace_period: 30s
|
||||
networks:
|
||||
financial: {internal: true}
|
||||
web: {}
|
||||
volumes:
|
||||
pg-data:
|
||||
pg-admin:
|
||||
pg-owner:
|
||||
pg-runtime:
|
||||
pg-backup:
|
||||
pg-tls:
|
||||
custody-node:
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
# Private pilot deployment: mock-only foundation
|
||||
|
||||
Status: **application and isolated PostgreSQL sandbox only**. This is not a real-XMR stack, a public node, or an approval to accept deposits. The first deployment increment intentionally has no Monero daemon, Wallet-RPC gateway, observer, deposit enrollment, or withdrawal execution.
|
||||
|
||||
## Start the isolated profile
|
||||
|
||||
Use [compose.custody-pilot.yaml](../compose.custody-pilot.yaml) with the stable project name `wikideck-xmr-pilot`. Do not combine it with [the existing local custody sandbox](../compose.custody.yaml), the disposable PostgreSQL fixture, or another pilot project name. Project-scoped volumes hold the pilot's PostgreSQL database, credentials, and node state separately.
|
||||
|
||||
Build/start with `docker compose -p wikideck-xmr-pilot -f compose.custody-pilot.yaml up -d --build`. Open `http://127.0.0.1:3423/` and confirm the page identifies **PRIVATE PILOT**. The service binds to loopback by default; set `WIKIDECK_PILOT_PORT` only when changing the local port.
|
||||
|
||||
The app and browser bundle both select the `pilot` identity. Startup requires `WIKIDECK_PILOT_MODE=private-mock-only`; payments are fixed to `mock`. The market sandbox and PostgreSQL schema are freshly bound to this distinct chain ID. Do not point this stack at existing testnet/mainnet node or custody volumes.
|
||||
|
||||
To stop while preserving pilot data, run `docker compose -p wikideck-xmr-pilot -f compose.custody-pilot.yaml down` without volume removal. Do not use `--volumes` unless the database, credentials, and node history are intentionally being destroyed.
|
||||
|
||||
## What this enables—and does not
|
||||
|
||||
The independent private chain uses a fixed pilot genesis time and its own deterministic chain/genesis identity. The app can run with the current sandbox features and mock balances while test cards and UI flows are exercised. Testnet and mainnet candidate identities and launch gates remain unchanged.
|
||||
|
||||
This stack does **not** create real Monero deposit addresses, scan a wallet, approve real credits, construct or relay withdrawals, or settle NFT sales against active consensus. Deposit enrollment and withdrawal request flags remain rejected outside the mainnet profile, and mainnet remains separately gated. Do not expose the service publicly or solicit funds. Wallet integration requires the later observer/gateway and schema/role work, then actual withdrawal execution and activated sale settlement described in [the implementation plan](real-xmr-pilot-plan.md).
|
||||
|
||||
## Initial checks
|
||||
|
||||
The network manifest reports `profile: pilot`, `network: wikideck-randomx-pilot-1`, and `launchStatus: private-pilot-mock-only`. Verify startup refuses a missing pilot-mode opt-in and any non-mock payment setting. Deployment tests check loopback binding, distinct profile selection, database secret isolation, and absence of Monero services. A successful mock deployment is only evidence for this first slice, not for real-XMR readiness.
|
||||
Executable
+119
@@ -0,0 +1,119 @@
|
||||
# Get the real-XMR market running
|
||||
|
||||
Updated: 2026-10-22. Implementation plan for a private, operator-supervised functional pilot—not a public production release. Wallet access and fund movement require separate explicit approval.
|
||||
|
||||
## Progress checkpoint — 2026-10-22
|
||||
|
||||
- **Step 1 started; mock-only foundation implemented.** Added a distinct `pilot` profile with its own network/genesis identity, explicit private-mock-only startup opt-in, separate local ledger/market/custody paths and browser wallet database, and a loopback-bound PostgreSQL sandbox stack. Setup and limits are in [the pilot deployment guide](real-xmr-pilot-deployment.md). Testnet v6 and mainnet candidate identities/gates were left intact.
|
||||
- **Step 1 is not complete.** This first stack has no Monero daemon, Wallet-RPC gateway, observer, real-fund deposit or withdrawal path. It is useful for isolated UI/mock-flow work only and must not receive funds.
|
||||
- **Validation:** pilot-specific network/startup/storage/Compose tests pass (10/10); pilot runtime Docker image builds. The attempted serial full offline suite had one native RandomX HTTP peer-gossip convergence failure in `tests/api.test.ts`; the other reported tests passed. Treat full-suite status as not yet clean and rerun after the next increment.
|
||||
- **Steps 2–5:** not started. Existing deposit/withdrawal APIs still require the separately gated mainnet profile; sale-lock transactions remain candidate-only and are not accepted by active consensus.
|
||||
|
||||
## Finish line
|
||||
|
||||
Two browser wallets complete this flow on one deployed stack:
|
||||
|
||||
**Buyer deposits XMR → available balance → fixed-price purchase → card ownership changes → seller receives proceeds → seller withdraws XMR.**
|
||||
|
||||
Use the existing pooled-custody architecture, PostgreSQL accounting, native Monero integration and UI. Do not restart architecture research, introduce another payment provider or build a general-purpose payment platform.
|
||||
|
||||
## Reuse what exists
|
||||
|
||||
- Exact atomic-XMR prices and fee calculations.
|
||||
- PostgreSQL schema v9 and accounting/reorg records.
|
||||
- Signed deposit enrollment, observation worker and reviewed credit/release commands.
|
||||
- Signed balance reads and withdrawal reservation/cancellation APIs.
|
||||
- Marketplace/custody UI and executable sale-lock/accounting models.
|
||||
|
||||
The missing work is integration plus two substantial features: **actual withdrawal execution and activated NFT sale settlement**. Current market commands remain sandbox-only; reserved withdrawals never send XMR; active consensus rejects sale-lock transactions.
|
||||
|
||||
## Smallest useful scope
|
||||
|
||||
One Linux host, one authoritative market writer, one treasury tool/worker, fixed-price sales and operator-controlled accounts initially. Manual signed deposit approvals and operator-triggered sends are sufficient. Reuse existing UI components; no admin dashboard, unattended treasury or auction system is needed.
|
||||
|
||||
## Implementation sequence
|
||||
|
||||
### 1. Make one pilot stack runnable
|
||||
|
||||
- Add an explicitly identified private pilot profile for the sale protocol, distinct from existing v6 and the final mainnet candidate.
|
||||
- Bind signed market/payment requests to the pilot chain and custodian; select the Monero network explicitly. Real XMR requires Monero mainnet, not a public Wikideck launch.
|
||||
- Give the pilot its own explicit genesis/startup configuration rather than inheriting the candidate's January 1, 2027 delay and public-peer requirements. Leave existing mainnet protections unchanged; do not fake review acknowledgements or alter v6 in place.
|
||||
- Add one pilot deployment definition using existing Docker patterns: app, PostgreSQL, observer and private Wallet-RPC gateway, with explicit connections to the daemon/watch-only wallet and separate spending wallet.
|
||||
- Include schema initialization, role grants and required configuration in a short setup procedure. Keep spending keys outside the web app and Wallet RPC off the public Internet.
|
||||
|
||||
**Progress:** A separate mock-only pilot network/profile, launch opt-in, profile-isolated local storage, PostgreSQL Compose deployment and setup guide now exist. This establishes the private application base only; the planned Monero daemon/observer/gateway and any real-fund integrations remain outstanding. See [pilot deployment status and startup](real-xmr-pilot-deployment.md).
|
||||
|
||||
**Result:** start the stack, open the UI, create two wallets and obtain a test card. Start preparing/syncing the Monero daemon and wallet while application development proceeds.
|
||||
|
||||
### 2. Connect deposits to spendable balances
|
||||
|
||||
- Add a deposit panel that signs the existing enrollment request and displays the allocated address.
|
||||
- Run the observer against the configured wallet; show pending and available balances.
|
||||
- Support the pilot identity in the existing prepare/stage/release workflow. Keep approvals operator-driven rather than adding automation.
|
||||
- Report capabilities truthfully as the flow becomes available; keep mock-credit controls unreachable in real-fund mode.
|
||||
|
||||
**Result:** a real Stagenet deposit is observed and approved into the buyer's available balance through the running application. This is private development progress, not a reason to solicit real deposits before cash-out works.
|
||||
|
||||
### 3. Finish withdrawals with an operator tool
|
||||
|
||||
- Extend signed reservations with durable preparation, prepared-transaction, relay and confirmation states, including an uncertain/review outcome.
|
||||
- Claim a request exclusively and block cancellation once preparation begins.
|
||||
- Construct/sign using the separate treasury wallet, enforcing destination, amount and maximum fee.
|
||||
- Record preparation intent before wallet side effects and retain the recoverable signed transaction identity/blob before relay. Reconcile ambiguous preparation before trying again.
|
||||
- Relay and track the stored transaction. Retries reuse that transaction rather than creating another payment.
|
||||
- Settle the reserved liability with the actual fee and return unused fee allowance; keep uncertain sends reserved.
|
||||
- Add browser withdrawal inputs, status and receipt. A CLI command can process the queue; no automatic payout scheduler is needed.
|
||||
|
||||
**Result:** deposit → signed withdrawal → operator send → XMR received, with correct balances and fees.
|
||||
|
||||
### 4. Connect purchases to NFT delivery and seller earnings
|
||||
|
||||
Start this alongside steps 2–3 after defining the pilot identity. Do not wait for a separate release-hardening phase.
|
||||
|
||||
- Integrate the existing lock/completion/cancellation/expiry model into pilot transaction validation, state commitments, mempool, canonical indexes/events, peer processing and restart replay. Use the existing RandomX ledger, not a toy substitute.
|
||||
- Adapt custody listing/purchase commands for real balances and fresh signatures. Atomically reserve buyer funds with the order; exclude mock funding/payment authorizations.
|
||||
- Collect seller/buyer lock consent, submit the lock and have the host attest completion only against the funded reservation.
|
||||
- Feed actual canonical outcomes into PostgreSQL settlement instead of synthetic rehearsal witnesses.
|
||||
- Credit seller, author and host once after configured confirmation depth; release buyer funds only after safe cancellation/expiry/non-admission evidence. Hold conflicting reorg outcomes rather than paying/refunding again.
|
||||
- Allow withdrawal of legitimate sale and fee earnings. The current own-deposit backing limit prevents a seller who never deposited from cashing out. Replace it with pooled-backing accounting covering liabilities, pending sends and fees—not merely removal of the check.
|
||||
- Apply existing accounting checks and incident holds to the connected sale and treasury writers.
|
||||
- Adapt the existing marketplace UI to show funded checkout, lock/completion progress, delivered ownership and proceeds.
|
||||
|
||||
**Result:** seller lists → buyer buys → card changes owner → exact seller/fee balances appear → seller withdraws, even without a prior seller deposit.
|
||||
|
||||
### 5. Exercise the complete deployed flow, then test real XMR
|
||||
|
||||
1. Run deposit → purchase → seller withdrawal on Stagenet using the actual browsers and deployment.
|
||||
2. Include focused implementation checks for duplicate requests, restart after preparation/relay, cancellation versus send and canonical sale disconnect. These protect the flow from duplicate money/asset movement; they are not a separate certification program.
|
||||
3. With explicit wallet/network/amount approval, configure Monero mainnet and repeat with operator-controlled accounts and a small amount sufficient for fees.
|
||||
|
||||
**Result:** a functional private real-XMR market with repeatable startup and operator commands. Public launch is outside this increment.
|
||||
|
||||
## Parallel work
|
||||
|
||||
| Workstream | Start | Delivers |
|
||||
| --- | --- | --- |
|
||||
| Pilot identity + deployment | First | Running stack and unblocked integration |
|
||||
| Deposit UI + reviewed credit | After identity/setup | Funded buyer balance |
|
||||
| Treasury tool + withdrawal UI | Alongside deposits | Actual cash-out |
|
||||
| Sale-lock activation + custody settlement | After identity is defined, alongside payment work | Paid NFT delivery and seller earnings |
|
||||
| Browser end-to-end flow | As slices connect | Deployable functional pilot |
|
||||
|
||||
Build each slice with its actual callers. Do not spend another increment adding a disconnected model or endpoint.
|
||||
|
||||
## Move out of the functional critical path
|
||||
|
||||
Track these separately as public-release/hardening follow-ups, rather than front-loading them as engineering phases:
|
||||
|
||||
- Broad audit/certification and release evidence packets.
|
||||
- Production-scale benchmarks, SLOs, HA, multi-host operations and additional orchestrators.
|
||||
- SBOM/provenance automation, comprehensive scanning programs and artifact signing.
|
||||
- Automated certificate rotation, backup scheduling, off-site recovery certification and full on-call systems.
|
||||
- Automatic credit approval, unattended signing, batching and admin dashboards.
|
||||
- Auctions, fiat conversion, alternate processors and final public-mainnet launch preparation.
|
||||
|
||||
Keep the functional essentials: signed authorization, correct network binding, private wallet access, exact balanced accounting, one-time settlement and recoverable sends. Deferring broader hardening does not make this production-ready; public/customer-fund rollout still needs the separate policy/legal/security decisions in [the production plan](production-plan.md).
|
||||
|
||||
## Next coding task
|
||||
|
||||
**Finish step 1's operational checks, then design the pilot's separate Wikideck-chain/Monero-mainnet financial identity and schema/role contract before connecting deposits.** Existing observer/deposit contracts are mainnet-specific, so do not simply enable their flags on the pilot. Keep the next deployment mock-only until network binding, reviewed credit, actual withdrawal execution, recovery, and sale settlement are connected. Continue with the operator treasury tool and active sale-lock settlement as parallel implementation tracks where the identity contract permits. Completion means a buyer purchasing a card and the seller receiving withdrawable XMR—not another scaffold or release checklist.
|
||||
+11
-2
@@ -1,13 +1,22 @@
|
||||
import { IS_MAINNET, NETWORK, PROFILE } from '../shared/network.ts';
|
||||
import { IS_MAINNET, IS_PILOT, NETWORK, PROFILE } from '../shared/network.ts';
|
||||
import { CHAIN_ID, GENESIS_HASH, GENESIS_TIME } from './testnet/consensus.ts';
|
||||
|
||||
export function launchManifest() {
|
||||
return { profile: PROFILE, network: NETWORK, chainId: CHAIN_ID, genesis: GENESIS_HASH,
|
||||
genesisTime: GENESIS_TIME, launchStatus: IS_MAINNET ? 'candidate-gated' : 'testnet',
|
||||
genesisTime: GENESIS_TIME, launchStatus: IS_MAINNET ? 'candidate-gated' : IS_PILOT ? 'private-pilot-mock-only' : 'testnet',
|
||||
paymentMode: 'mock', audited: false };
|
||||
}
|
||||
/** Operator attestation is an accident-prevention gate, NOT proof of a security audit. */
|
||||
export function assertLaunchAllowed(env: NodeJS.ProcessEnv = process.env, now = Date.now()) {
|
||||
if (IS_PILOT) {
|
||||
if (env.WIKIDECK_PILOT_MODE !== 'private-mock-only') {
|
||||
throw new Error('Private pilot requires WIKIDECK_PILOT_MODE=private-mock-only');
|
||||
}
|
||||
if ((env.WIKIDECK_PAYMENT_MODE ?? 'mock') !== 'mock') {
|
||||
throw new Error('Private pilot supports mock payments only; live XMR remains disabled');
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!IS_MAINNET) return;
|
||||
if (env.WIKIDECK_MAINNET_LAUNCH !== CHAIN_ID || env.WIKIDECK_MAINNET_REVIEW_ACK !== 'independent-review-complete') {
|
||||
throw new Error(`Mainnet candidate is launch-gated. Review the launch checklist; explicit chain approval required: ${CHAIN_ID}`);
|
||||
|
||||
@@ -4,10 +4,12 @@ import { join } from 'node:path';
|
||||
import { CustodyMarket } from './custody.ts';
|
||||
import { PostgresCustody, type CustodyOptions } from './postgres-custody.ts';
|
||||
import { custodyStorage, postgresConfig } from './postgres-config.ts';
|
||||
import { PROFILE } from '../../shared/network.ts';
|
||||
|
||||
/** No automatic SQLite reinterpretation or fallback after a database connection/configuration failure. */
|
||||
export async function openCustodyStorage(directory: string, options: CustodyOptions, env = process.env): Promise<CustodyMarket | PostgresCustody> {
|
||||
const storage = custodyStorage(env), marker = join(directory, 'testnet', 'custody-postgres.json');
|
||||
const profileDirectory = join(directory, PROFILE);
|
||||
const storage = custodyStorage(env), marker = join(profileDirectory, 'custody-postgres.json');
|
||||
const observerMode = env.WIKIDECK_CUSTODY_CANONICAL_REHEARSAL;
|
||||
if (observerMode !== undefined && observerMode !== '0' && observerMode !== '1') {
|
||||
throw new Error('WIKIDECK_CUSTODY_CANONICAL_REHEARSAL must be exactly 0 or 1');
|
||||
@@ -16,7 +18,7 @@ export async function openCustodyStorage(directory: string, options: CustodyOpti
|
||||
throw new Error('Canonical observer rehearsal requires PostgreSQL and the disposable database test guard');
|
||||
}
|
||||
const custodyOptions = observerMode === '1' ? { ...options, disposableSaleRehearsal: true } : options;
|
||||
await mkdir(join(directory, 'testnet'), { recursive: true, mode: 0o700 });
|
||||
await mkdir(profileDirectory, { recursive: true, mode: 0o700 });
|
||||
let existing: { version: number; custodianId: string } | undefined;
|
||||
try { existing = JSON.parse(await readFile(marker, 'utf8')); }
|
||||
catch (e) { if ((e as NodeJS.ErrnoException).code !== 'ENOENT') throw e; }
|
||||
@@ -24,7 +26,7 @@ export async function openCustodyStorage(directory: string, options: CustodyOpti
|
||||
if (existing) throw new Error('Custody data directory is bound to PostgreSQL; SQLite fallback prohibited');
|
||||
return CustodyMarket.open(directory, custodyOptions);
|
||||
}
|
||||
const source = join(directory, 'testnet', 'custody-sandbox.sqlite');
|
||||
const source = join(profileDirectory, 'custody-sandbox.sqlite');
|
||||
let frozen: string | undefined;
|
||||
try {
|
||||
const info = await lstat(source);
|
||||
@@ -44,7 +46,7 @@ export async function openCustodyStorage(directory: string, options: CustodyOpti
|
||||
try { await handle.writeFile(JSON.stringify({ version: 1, custodianId: market.id })); await handle.sync(); }
|
||||
finally { await handle.close(); }
|
||||
if (process.platform !== 'win32') {
|
||||
const parent = await open(join(directory, 'testnet'), 'r');
|
||||
const parent = await open(profileDirectory, 'r');
|
||||
try { await parent.sync(); } finally { await parent.close(); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { DatabaseSync } from 'node:sqlite';
|
||||
import { createHash, randomUUID } from 'node:crypto';
|
||||
import { mkdir, chmod } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { IS_MAINNET } from '../../shared/network.ts';
|
||||
import { IS_MAINNET, PROFILE } from '../../shared/network.ts';
|
||||
import { NETWORK } from '../../shared/testnet.ts';
|
||||
import { ADDITIONAL_MARKET_FEE } from '../../shared/market.ts';
|
||||
import { allocateXmrSale } from '../../shared/xmr-pricing.ts';
|
||||
@@ -50,7 +50,7 @@ export class CustodyMarket {
|
||||
if (IS_MAINNET) throw new Error('Custody sandbox cannot run on mainnet');
|
||||
await validateMarketPolicy(options);
|
||||
if (!Number.isSafeInteger(options.feeBps) || options.feeBps < 0 || options.feeBps > 5000) throw new Error('Invalid custody fee');
|
||||
const folder = join(directory, 'testnet');
|
||||
const folder = join(directory, PROFILE);
|
||||
await mkdir(folder, { recursive: true, mode: 0o700 });
|
||||
const file = join(folder, 'custody-sandbox.sqlite');
|
||||
const db = new DatabaseSync(file);
|
||||
|
||||
@@ -16,6 +16,7 @@ import { RpcError } from '../resources.ts';
|
||||
import type { CanonicalEvents, IndexedNft } from '../testnet/canonical-index.ts';
|
||||
import { z } from 'zod';
|
||||
import { operatorMarketPolicy, resolvedMarketPolicy, permitsMarketAction, validateMarketPolicy, type MarketPolicy } from './policy.ts';
|
||||
import { PROFILE } from '../../shared/network.ts';
|
||||
|
||||
type Chain = { snapshot(): TestnetSnapshot; summary?(): { tip: string; revision: number; height?: number };
|
||||
submitTransfer(raw: unknown): Promise<string>; nft?(token: string): IndexedNft | undefined;
|
||||
@@ -112,7 +113,7 @@ export class Market {
|
||||
static async open(directory: string, chain: Chain, config = marketConfig(), now = Date.now,
|
||||
reconciliation: { worker?: boolean } = {}): Promise<Market> {
|
||||
await validateMarketPolicy(config);
|
||||
const folder = join(directory, NETWORK.includes('mainnet') ? 'mainnet' : 'testnet');
|
||||
const folder = join(directory, PROFILE);
|
||||
await mkdir(folder, { recursive: true, mode: 0o700 });
|
||||
const file = join(folder, 'market.sqlite');
|
||||
const db = new DatabaseSync(file);
|
||||
|
||||
+6
-4
@@ -1,4 +1,4 @@
|
||||
import { IS_MAINNET } from '../shared/network.ts';
|
||||
import { IS_MAINNET, IS_PILOT } from '../shared/network.ts';
|
||||
import { ADDITIONAL_MARKET_FEE } from '../shared/market.ts';
|
||||
import { assertLaunchAllowed, launchManifest } from './launch.ts';
|
||||
import { marketConfig } from './market/store.ts';
|
||||
@@ -7,15 +7,17 @@ export type LaunchCheck = { id: string; status: 'pass' | 'blocked' | 'unverified
|
||||
/** Read-only local assessment. Never opens a ledger, starts PoW, contacts peers, or sets approvals.
|
||||
* Configured approvals cannot resolve findings that require new code or independent evidence. */
|
||||
export function launchPreflight(env: NodeJS.ProcessEnv = process.env, now = Date.now()) {
|
||||
const checks: LaunchCheck[] = [{ id: 'network-selection', status: IS_MAINNET ? 'pass' : 'blocked',
|
||||
detail: IS_MAINNET ? 'Separate mainnet candidate selected.' : 'Testnet selected; this is not a production profile.' }];
|
||||
const checks: LaunchCheck[] = [{ id: 'network-selection', status: IS_MAINNET || IS_PILOT ? 'pass' : 'blocked',
|
||||
detail: IS_MAINNET ? 'Separate mainnet candidate selected.' : IS_PILOT
|
||||
? 'Private pilot chain selected; this is not a public or production profile.'
|
||||
: 'Testnet selected; this is not a production profile.' }];
|
||||
try {
|
||||
marketConfig(env);
|
||||
checks.push({ id: 'mock-payment-boundary', status: 'pass', detail: 'Only mock payments are supported; market configuration is valid.' });
|
||||
} catch {
|
||||
checks.push({ id: 'mock-payment-boundary', status: 'blocked', detail: 'Invalid market configuration or unsupported live payment mode. No secret values are included in this report.' });
|
||||
}
|
||||
if (IS_MAINNET) {
|
||||
if (IS_MAINNET || IS_PILOT) {
|
||||
try {
|
||||
assertLaunchAllowed(env, now);
|
||||
checks.push({ id: 'operator-startup-gates', status: 'pass', detail: 'Local startup gates satisfied; this does not establish production readiness or peer independence.' });
|
||||
|
||||
@@ -33,7 +33,7 @@ import { createMoneroDepositAddressRpc, loadMoneroObserverConfig } from '../mark
|
||||
import { loadMoneroMtlsTransport, loadMoneroMtlsTransportConfig } from '../market/monero-mtls-transport.ts';
|
||||
import { postgresConfig } from '../market/postgres-config.ts';
|
||||
import { validateMarketPolicy } from '../market/policy.ts';
|
||||
import { IS_MAINNET, PROFILE } from '../../shared/network.ts';
|
||||
import { IS_MAINNET, IS_PILOT, PROFILE } from '../../shared/network.ts';
|
||||
import { assertLaunchAllowed, launchManifest } from '../launch.ts';
|
||||
import { resourceConfig, RpcBudget, RpcError } from '../resources.ts';
|
||||
import { trustedProxies, operationsPort, startOperations } from '../operations.ts';
|
||||
@@ -353,7 +353,7 @@ app.use((err:Error & {status?:number},_req:express.Request,res:express.Response,
|
||||
});
|
||||
const port=Number(process.env.PORT??3001),host=process.env.HOST??'127.0.0.1';
|
||||
const server=app.listen(port,host,()=>{
|
||||
const a=server.address();console.log(`Wikideck RandomX ${IS_MAINNET?'mainnet candidate':'testnet'}: http://${host}:${typeof a==='object'&&a?a.port:port} · chain ${CHAIN_ID} · ${ledger.snapshot().blocks.length} blocks`);
|
||||
const a=server.address();console.log(`Wikideck RandomX ${IS_MAINNET?'mainnet candidate':IS_PILOT?'private pilot':'testnet'}: http://${host}:${typeof a==='object'&&a?a.port:port} · chain ${CHAIN_ID} · ${ledger.snapshot().blocks.length} blocks`);
|
||||
peers.start();
|
||||
});
|
||||
const operations=startOperations(opsPort,()=>({network:NETWORK,paymentMode:'mock',
|
||||
|
||||
+10
-5
@@ -1,13 +1,18 @@
|
||||
/** Selection is fixed at server startup / browser build, never learned from an untrusted RPC. */
|
||||
export function networkProfile(value: string | undefined): 'testnet' | 'mainnet' {
|
||||
export type NetworkProfile = 'testnet' | 'mainnet' | 'pilot';
|
||||
export function networkProfile(value: string | undefined): NetworkProfile {
|
||||
const profile = value ?? 'testnet';
|
||||
if (profile !== 'testnet' && profile !== 'mainnet') throw new Error('WIKIDECK_NETWORK must be testnet or mainnet');
|
||||
if (profile !== 'testnet' && profile !== 'mainnet' && profile !== 'pilot') {
|
||||
throw new Error('WIKIDECK_NETWORK must be testnet, mainnet, or pilot');
|
||||
}
|
||||
return profile;
|
||||
}
|
||||
export const PROFILE = networkProfile(typeof process !== 'undefined' && process.versions?.node
|
||||
? process.env.WIKIDECK_NETWORK : import.meta.env?.VITE_WIKIDECK_NETWORK);
|
||||
export const IS_MAINNET = PROFILE === 'mainnet';
|
||||
export const NETWORK = IS_MAINNET ? 'wikideck-randomx-mainnet-1' as const : 'wikideck-randomx-testnet-6' as const;
|
||||
export const IS_PILOT = PROFILE === 'pilot';
|
||||
export const NETWORK = IS_MAINNET ? 'wikideck-randomx-mainnet-1' as const
|
||||
: IS_PILOT ? 'wikideck-randomx-pilot-1' as const : 'wikideck-randomx-testnet-6' as const;
|
||||
// Candidate mainnet genesis: 2027-01-01 00:00 UTC. NOT a commitment to a launch date.
|
||||
export const NETWORK_GENESIS_TIME = IS_MAINNET ? 1_798_761_600 : 1_792_411_200;
|
||||
export const NETWORK_LABEL = IS_MAINNET ? 'MAINNET CANDIDATE' : 'TESTNET';
|
||||
export const NETWORK_GENESIS_TIME = IS_MAINNET ? 1_798_761_600 : IS_PILOT ? 1_792_627_200 : 1_792_411_200;
|
||||
export const NETWORK_LABEL = IS_MAINNET ? 'MAINNET CANDIDATE' : IS_PILOT ? 'PRIVATE PILOT' : 'TESTNET';
|
||||
|
||||
+15
-15
@@ -12,7 +12,7 @@ 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 { IS_MAINNET, IS_PILOT, NETWORK_LABEL } from '../shared/network.ts';
|
||||
import { StateClient } from '../shared/rpc-client.ts';
|
||||
import './App.css';
|
||||
import './testnet.css';
|
||||
@@ -166,18 +166,18 @@ 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>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-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>
|
||||
<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>
|
||||
<div className="main-shell">
|
||||
<header className="topbar"><div className="breadcrumbs">Explore <ChevronRight size={13} /><span>{view}</span></div><div className="top-actions"><span className="testnet-brand">RandomX <b>{NETWORK_LABEL}</b></span><ThemePicker /><span className="network-pill"><span className={`network-dot ${!rpcAvailable || !snapshot?.peers ? 'disconnected' : ''}`} />{peerLabel}</span><button className="wallet-button" onClick={openWallet}><span className="wallet-avatar"><WalletIcon size={14} /></span>{wallet ? short(wallet.address) : 'Open wallet'}<ChevronRight size={14} /></button></div></header>
|
||||
<main>
|
||||
<div className="testnet-status" role="status"><span><span className={`network-dot ${rpcAvailable ? '' : 'disconnected'}`} /> RPC {rpcAvailable ? 'available' : 'unavailable'}</span><span>{peerLabel}</span><span>{IS_MAINNET?'Gated mainnet candidate · unaudited · mock payments only':'Unbootstrapped testnet · no public mainnet'}</span>{snapshot && !rpcAvailable && <strong>Showing last known state</strong>}</div>
|
||||
<div className="testnet-status" role="status"><span><span className={`network-dot ${rpcAvailable ? '' : 'disconnected'}`} /> RPC {rpcAvailable ? 'available' : 'unavailable'}</span><span>{peerLabel}</span><span>{IS_MAINNET?'Gated mainnet candidate · unaudited · mock payments only':IS_PILOT?'Private pilot · mock custody only · no real XMR':'Unbootstrapped testnet · no public mainnet'}</span>{snapshot && !rpcAvailable && <strong>Showing last known state</strong>}</div>
|
||||
{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 /> 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-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-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,13 +187,13 @@ 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" /> 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><backup.json></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>
|
||||
<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 & 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 & 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><backup.json></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 & 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>
|
||||
<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>
|
||||
</>}
|
||||
{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">
|
||||
@@ -217,14 +217,14 @@ export default function App() {
|
||||
{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. Collect. Make it yours.</h2>
|
||||
<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>
|
||||
<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><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 & 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 & 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>
|
||||
<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>
|
||||
<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>
|
||||
</>
|
||||
: 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></>
|
||||
|
||||
+2
-2
@@ -154,8 +154,8 @@ export function Marketplace({ wallet, snapshot, rpcAvailable, onNotice }: Props)
|
||||
return <section className="marketplace">
|
||||
{page&&(page.info.marketEnabled===false||page.info.tradingEnabled===false)&&<p className="market-simulation" role="status">{page.info.marketEnabled===false?'This peer does not offer a public marketplace.':'This peer has paused new trading.'} Existing orders, cancellations and resolution remain accessible through Your activity.</p>}
|
||||
{page?.info.hostFeeAddress&&<p className="market-fee-recipient">Host fee wallet (public payout recipient, not a deposit address): <span>{page.info.hostFeeAddress}</span></p>}
|
||||
<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. Prices are listed directly in XMR atomic units (12 decimal places). Every peer can host its own market; listings and orders remain local to that host.</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>
|
||||
<header className="market-hero"><div><div className="section-eyebrow">A MARKET RUN BY THIS PEER</div><h1>Find your next card.<br/><span>Trade with your people.</span></h1><p>Browse fixed-price cards or join a timed auction. Prices are shown in XMR. Each peer runs its own market, so listings and orders stay with this host.</p></div><div className="market-symbol"><ShoppingBag size={52}/><span>PEER-HOSTED · COMMUNITY-DRIVEN</span></div></header>
|
||||
<p className="muted">The host sets its market fee; a separate 1% author fee also applies. Both are taken from simulated seller proceeds and shown before signing. Community curation and custom trading-card games 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','custody'] as const).map(t=><button key={t} className={tab===t?'selected':''} onClick={()=>setTab(t)}>{t==='browse'?'Explore market':t==='sell'?'Sell a card':t==='custody'?'Custody preview':'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>
|
||||
{tab==='custody'&&<CustodyPanel wallet={wallet} snapshot={snapshot} rpcAvailable={rpcAvailable}/>}
|
||||
|
||||
+3
-3
@@ -206,17 +206,17 @@ export function TradeDesk({ wallet, snapshot, rpcAvailable, onNotice }: Props) {
|
||||
: tx.cardHash === card.block.tokenId)));
|
||||
|
||||
return <section className="trade-desk">
|
||||
<header className="trade-heading"><div><div className="section-eyebrow">PLAYER-TO-PLAYER · ATOMIC SETTLEMENT</div><h1>Trade decks</h1><p>Browse a public wallet deck, choose multiple cards on both sides, then exchange a signed offer. The complete trade settles as one indivisible on-chain transaction.</p></div><span className="trade-limit">Up to {CONSENSUS.maxTradeCards} cards per side</span></header>
|
||||
<header className="trade-heading"><div><div className="section-eyebrow">PLAYER-TO-PLAYER · THE WHOLE SWAP OR NONE</div><h1>Build a card-for-card trade</h1><p>Choose cards from each collection, agree on the swap, and both sign. The network settles the entire bundle together—never a partial trade.</p></div><span className="trade-limit">Up to {CONSENSUS.maxTradeCards} cards per side</span></header>
|
||||
{!wallet && <p className="trade-warning">Open or restore your wallet to build and accept trade offers.</p>}
|
||||
{!rpcAvailable && <p className="trade-warning">Node is unavailable. Reconnect before creating or accepting a trade.</p>}
|
||||
<section className="trade-lookup"><label htmlFor="player-address">PLAYER RECEIVE ADDRESS</label><div><input id="player-address" value={address} onChange={event => setAddress(event.target.value)} placeholder="Paste a 64-character wallet address" maxLength={64} autoComplete="off"/><button className="button dark" onClick={loadDeck} disabled={loadingDeck || !rpcAvailable}>{loadingDeck?<LoaderCircle className="spin" size={16}/>:<RefreshCw size={16}/>}Load deck</button></div><p>Decks are public canonical ownership at the node’s reported chain tip. Viewing an address can reveal its holdings.</p></section>
|
||||
<section className="trade-lookup"><label htmlFor="player-address">OTHER PLAYER’S WALLET ADDRESS</label><div><input id="player-address" value={address} onChange={event => setAddress(event.target.value)} placeholder="Paste a 64-character wallet address" maxLength={64} autoComplete="off"/><button className="button dark" onClick={loadDeck} disabled={loadingDeck || !rpcAvailable}>{loadingDeck?<LoaderCircle className="spin" size={16}/>:<RefreshCw size={16}/>}View collection</button></div><p>Wallet holdings are public on this chain. Looking up an address reveals its confirmed cards at the node’s current chain tip.</p></section>
|
||||
<div className="trade-columns">
|
||||
<DeckList title="Your confirmed cards · offered" cards={mineEligible} selected={offered} onToggle={id => toggle(setOffered, offered, id)} disabled={!wallet || busy}/>
|
||||
<div className="trade-arrow"><ArrowLeftRight size={24}/><span>Swap</span></div>
|
||||
<DeckList title={remoteDeck?`Player ${short(remoteDeck.address)} · requested`:'Other player’s deck'} cards={otherEligible} selected={requested} onToggle={id => toggle(setRequested, requested, id)} disabled={busy || !remoteDeck}/>
|
||||
</div>
|
||||
<div className="trade-actions"><button className="button dark" onClick={createOffer} disabled={!wallet || !rpcAvailable || !remoteDeck || !offered.size || !requested.size || busy}><Check size={16}/>Sign offer</button><span>{offered.size} offered · {requested.size} requested</span></div>
|
||||
<section className="trade-import"><div className="section-eyebrow">OFFER JSON · COPY OR PASTE</div><p>Only the proposing wallet signs when creating an offer. It is not active until the addressed player reviews and accepts it. Acceptance signs all requested cards and submits one atomic trade bundle; stale offers are rejected.</p><textarea aria-label="Trade offer JSON" value={offerText} onChange={event => setOfferText(event.target.value)} placeholder="Create an offer above, or paste a trade offer from another player." spellCheck={false}/>{parsedOffer&&<div className="trade-review"><strong>Review before signing</strong><p>{short(parsedOffer.proposer)} offers {parsedOffer.offered.map(leg=>offerTitle(leg.cardHash)).join(', ')} to {short(parsedOffer.counterparty)} for {parsedOffer.requested.map(leg=>offerTitle(leg.cardHash)).join(', ')}.</p><small>Offer signatures are checked by the node. Acceptance transfers all listed cards together or none.</small></div>}{offerError&&<p className="trade-error" role="alert">{offerError}</p>}<div className="trade-actions"><button className="button outline" onClick={copyOffer} disabled={!offerText}><Clipboard size={16}/>Copy offer</button><button className="button dark" onClick={acceptOffer} disabled={!wallet || !rpcAvailable || !parsedOffer || busy}>{busy?<LoaderCircle className="spin" size={16}/>:<Send size={16}/>}Accept and submit atomic trade</button></div>
|
||||
<section className="trade-import"><div className="section-eyebrow">SHARE AN OFFER</div><p>Your wallet signs the offer; it becomes actionable only after the named player reviews and accepts it. Offers are shared as text, so send them through a channel you trust. Both signatures are checked before the complete card swap is submitted.</p><textarea aria-label="Trade offer JSON" value={offerText} onChange={event => setOfferText(event.target.value)} placeholder="Create an offer above, or paste a trade offer from another player." spellCheck={false}/>{parsedOffer&&<div className="trade-review"><strong>Review before signing</strong><p>{short(parsedOffer.proposer)} offers {parsedOffer.offered.map(leg=>offerTitle(leg.cardHash)).join(', ')} to {short(parsedOffer.counterparty)} for {parsedOffer.requested.map(leg=>offerTitle(leg.cardHash)).join(', ')}.</p><small>The node checks both signatures. All listed cards move together or none do.</small></div>}{offerError&&<p className="trade-error" role="alert">{offerError}</p>}<div className="trade-actions"><button className="button outline" onClick={copyOffer} disabled={!offerText}><Clipboard size={16}/>Copy offer</button><button className="button dark" onClick={acceptOffer} disabled={!wallet || !rpcAvailable || !parsedOffer || busy}>{busy?<LoaderCircle className="spin" size={16}/>:<Send size={16}/>}Accept and submit trade</button></div>
|
||||
{remoteDeck&&<div className="trade-tip">Deck tip: <code>{short(remoteDeck.tip)}</code> · showing {remoteDeck.nfts.length} of {remoteDeck.total}; ownership and maturity re-checked when signing.{remoteDeck.next!==null&&<button className="text-button" onClick={loadMore} disabled={loadingMore}>{loadingMore?<LoaderCircle className="spin" size={14}/>:null}Load more cards</button>}</div>}
|
||||
</section>
|
||||
</section>;
|
||||
|
||||
@@ -13,6 +13,11 @@
|
||||
.native-memory strong { color: var(--ctp-lavender); }
|
||||
.native-memory span { color: var(--ctp-subtext0); }
|
||||
.native-memory strong:not(:first-child) { margin-top: 6px; }
|
||||
.mining-details { margin-top: 18px; color: var(--ctp-subtext0); font-size: 12px; line-height: 1.8; }
|
||||
.mining-details > summary, .guide-steps details > summary { width: fit-content; color: var(--theme-accent); cursor: pointer; font-weight: 600; text-underline-offset: 3px; }
|
||||
.guide-steps details { margin-top: 10px; padding: 10px 12px; border: 1px solid var(--theme-border); border-radius: 8px; background: var(--ctp-mantle); }
|
||||
.guide-steps details p { margin: 8px 0 0; }
|
||||
.mining-details > summary:focus-visible, .guide-steps details > summary:focus-visible { outline: 2px solid var(--theme-accent); outline-offset: 4px; border-radius: 2px; }
|
||||
.native-steps { padding-left: 20px; margin: 18px 0; color: var(--ctp-subtext1); font-size: 12px; line-height: 1.8; }
|
||||
.native-steps li + li { margin-top: 8px; }
|
||||
.native-command { margin: 14px 0; padding: 16px; border: 1px solid var(--ctp-surface1); border-radius: 8px; background: var(--ctp-crust); color: var(--ctp-green); white-space: pre-wrap; overflow-wrap: anywhere; font-size: 12px; line-height: 1.9; user-select: text; }
|
||||
|
||||
+3
-2
@@ -1,5 +1,5 @@
|
||||
import { decryptWallet, encryptWallet, type WalletBackup } from '../shared/wallet-backup.ts';
|
||||
import { IS_MAINNET } from '../shared/network.ts';
|
||||
import { IS_MAINNET, PROFILE } from '../shared/network.ts';
|
||||
|
||||
export type Wallet = { publicKey: string; address: string };
|
||||
type Keys = Awaited<ReturnType<typeof decryptWallet>>;
|
||||
@@ -12,7 +12,8 @@ let session: { keys: Keys; handle: Wallet; revision: string; opened: number; tou
|
||||
let epoch = 0;
|
||||
const listeners = new Set<() => void>();
|
||||
const isVault = (value: Stored): value is Vault => 'format' in value && value.format === 'wikideck-local-vault';
|
||||
const DATABASE = IS_MAINNET ? 'wikideck-mainnet-wallet-v1' : 'wikideck-testnet-wallet-v2';
|
||||
const DATABASE = IS_MAINNET ? 'wikideck-mainnet-wallet-v1'
|
||||
: PROFILE === 'testnet' ? 'wikideck-testnet-wallet-v2' : 'wikideck-pilot-wallet-v1';
|
||||
const LOCK = `${DATABASE}:identity`;
|
||||
const encode = (bytes: ArrayBuffer) => btoa(String.fromCharCode(...new Uint8Array(bytes)));
|
||||
|
||||
|
||||
@@ -21,8 +21,36 @@ test('default testnet v6 manifest is unchanged and unknown network profiles fail
|
||||
assert.equal(GENESIS_TIME, 1_792_411_200);
|
||||
assert.equal(CHAIN_ID, hash(JSON.stringify(['wikideck-randomx-testnet-6', CONSENSUS, 1_792_411_200, MAX_TARGET])));
|
||||
assert.equal(networkProfile(undefined), 'testnet');
|
||||
assert.equal(networkProfile('pilot'), 'pilot');
|
||||
assert.throws(() => networkProfile('production')); assert.throws(() => networkProfile(''));
|
||||
});
|
||||
test('private pilot is a separate deterministic chain with an explicit mock-only startup gate', () => {
|
||||
const output = JSON.parse(script(`
|
||||
import { NETWORK, NETWORK_GENESIS_TIME, NETWORK_LABEL } from './shared/network.ts';
|
||||
import { CHAIN_ID, GENESIS_HASH, GENESIS_TIME } from './server/testnet/consensus.ts';
|
||||
import { launchManifest, assertLaunchAllowed } from './server/launch.ts';
|
||||
const allowed = (env) => { try { assertLaunchAllowed(env); return true; } catch { return false; } };
|
||||
console.log(JSON.stringify({ NETWORK, NETWORK_GENESIS_TIME, NETWORK_LABEL, CHAIN_ID, GENESIS_HASH, GENESIS_TIME,
|
||||
manifest:launchManifest(), missing:allowed({}), approved:allowed({WIKIDECK_PILOT_MODE:'private-mock-only'}),
|
||||
livePayment:allowed({WIKIDECK_PILOT_MODE:'private-mock-only',WIKIDECK_PAYMENT_MODE:'live'}) }));
|
||||
`, { WIKIDECK_NETWORK: 'pilot' }));
|
||||
assert.equal(output.NETWORK, 'wikideck-randomx-pilot-1');
|
||||
assert.equal(output.GENESIS_TIME, 1_792_627_200);
|
||||
assert.equal(output.NETWORK_LABEL, 'PRIVATE PILOT');
|
||||
assert.equal(output.manifest.launchStatus, 'private-pilot-mock-only');
|
||||
assert.equal(output.manifest.paymentMode, 'mock');
|
||||
assert.equal(output.missing, false);
|
||||
assert.equal(output.approved, true);
|
||||
assert.equal(output.livePayment, false);
|
||||
assert.notEqual(output.CHAIN_ID, CHAIN_ID);
|
||||
assert.notEqual(output.GENESIS_HASH, GENESIS_HASH);
|
||||
const missingMode = spawnSync(process.execPath, ['--experimental-strip-types', 'server/index.ts'], {
|
||||
env: { ...process.env, WIKIDECK_NETWORK: 'pilot', WIKIDECK_PILOT_MODE: '' },
|
||||
encoding: 'utf8', timeout: 30000,
|
||||
});
|
||||
assert.notEqual(missingMode.status, 0);
|
||||
assert.match(missingMode.stderr, /Private pilot requires WIKIDECK_PILOT_MODE/);
|
||||
});
|
||||
test('mainnet candidate has distinct deterministic genesis, signatures and strict transaction schemas', () => {
|
||||
const output = JSON.parse(script(`
|
||||
import { NETWORK, nftTransferMessage } from './shared/testnet.ts';
|
||||
@@ -86,6 +114,24 @@ test('mainnet ledger and market files use isolated folders; testnet backup heade
|
||||
} finally { await rm(dir, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test('pilot ledger and market files use their own profile directory', async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'wikideck-pilot-network-'));
|
||||
try {
|
||||
const result = JSON.parse(script(`
|
||||
import { Ledger } from './server/testnet/ledger.ts';
|
||||
import { Market } from './server/market/store.ts';
|
||||
import { existsSync } from 'node:fs'; import { join } from 'node:path';
|
||||
const ledger=await Ledger.open(process.env.PROFILE_TEST_DIR,{hash:async()=>{throw Error('unused')},close(){}});
|
||||
const market=await Market.open(process.env.PROFILE_TEST_DIR,ledger,undefined,Date.now,{worker:false});
|
||||
const result={ledger:existsSync(join(process.env.PROFILE_TEST_DIR,'pilot','ledger.sqlite')),
|
||||
market:existsSync(join(process.env.PROFILE_TEST_DIR,'pilot','market.sqlite')),
|
||||
testnet:existsSync(join(process.env.PROFILE_TEST_DIR,'testnet'))};
|
||||
market.close();ledger.close();console.log(JSON.stringify(result));
|
||||
`, { WIKIDECK_NETWORK: 'pilot', WIKIDECK_PILOT_MODE: 'private-mock-only', PROFILE_TEST_DIR: dir }));
|
||||
assert.deepEqual(result, { ledger: true, market: true, testnet: false });
|
||||
} finally { await rm(dir, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test('operator preflight is read-only, fails closed, and does not disclose credentials', async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'wikideck-preflight-'));
|
||||
try {
|
||||
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
|
||||
test('private pilot Compose stack is loopback-bound, isolated, and mock-only', async () => {
|
||||
const compose = (await readFile(new URL('../compose.custody-pilot.yaml', import.meta.url), 'utf8')).replaceAll('\r\n', '\n');
|
||||
assert.match(compose, /WIKIDECK_NETWORK: pilot/);
|
||||
assert.match(compose, /WIKIDECK_PILOT_MODE: private-mock-only/);
|
||||
assert.match(compose, /WIKIDECK_PAYMENT_MODE: mock/);
|
||||
assert.match(compose, /WIKIDECK_CUSTODY_STORAGE: postgres/);
|
||||
assert.match(compose, /WIKIDECK_CUSTODY_MODE: sandbox/);
|
||||
assert.match(compose, /WIKIDECK_LOCAL_PG_BOOTSTRAP: sandbox-only/);
|
||||
assert.match(compose, /127\.0\.0\.1:\$\{WIKIDECK_PILOT_PORT:-3423\}:3001/);
|
||||
assert.match(compose, /financial: \{internal: true\}/);
|
||||
assert.match(compose, /pg-runtime:\/secrets\/runtime:ro/);
|
||||
assert.doesNotMatch(compose, /POSTGRES_PASSWORD:\s|5432:5432/i);
|
||||
assert.doesNotMatch(compose, /^\s+(?:monero|wallet-rpc|observer):/im);
|
||||
const database = compose.split(' database:')[1].split(' migrate:')[0];
|
||||
assert.doesNotMatch(database, /ports:/);
|
||||
const app = compose.split(' wikideck:')[1].split('networks:\n financial:')[0];
|
||||
assert.doesNotMatch(app, /pg-admin:|pg-owner:|pg-tls:/);
|
||||
});
|
||||
@@ -10,17 +10,21 @@ test('project overview distinguishes mining rewards, peer markets, planned TCGs
|
||||
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 is mock-only/);
|
||||
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,/does not yet contain a project LICENSE file/);
|
||||
assert.doesNotMatch(readme,/## Mine only by explicit choice/);
|
||||
});
|
||||
test('homepage explains the roadmap without opt-in marketing or implying live XMR checkout',async()=>{
|
||||
test('homepage sells the collector vision while disclosing mining and market limits',async()=>{
|
||||
const app=await readFile(new URL('../src/App.tsx',import.meta.url),'utf8');
|
||||
assert.match(app,/Mine a block\. Earn a Wikipedia NFT card/);
|
||||
assert.match(app,/ON THE ROADMAP · NOT PLAYABLE YET/);
|
||||
assert.match(app,/peer-hosted custom TCGs are planned/);
|
||||
assert.match(app,/today payments and fees are simulated/);
|
||||
assert.doesNotMatch(app,/EXPLICIT OPT-IN|opt-in RandomX|No browser mining|Only by choice/);
|
||||
assert.match(app,/Mine with your CPU/);
|
||||
assert.match(app,/Build a deck that feels like yours/);
|
||||
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,/<details className="mining-details"><summary>Want to run a miner\? Setup & 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()=>{
|
||||
|
||||
Reference in New Issue
Block a user