Deposit shows a scannable QR, because nobody types a 400-character invoice at a party. The QR encoder is written rather than imported: a CDN script is a dependency on the outside world, and this box has to work on a network with no internet. Its tests caught a real bug — the format-information loop wrote eight cells down column 8, but the eighth is the dark module, which is fixed. Overwriting it produces symbols some readers reject. The deposit and withdraw cards stay hidden unless the server reports a node, so a play-money deployment does not advertise a deposit it cannot honour. Settlement is polled and confirmed server-side against the node, so the client cannot claim a payment that never arrived. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
300 lines
11 KiB
HTML
300 lines
11 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, user-scalable=no">
|
||
<meta name="theme-color" content="#000000">
|
||
<meta name="mobile-web-app-capable" content="yes">
|
||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||
<title>QUANTUM ARCADE</title>
|
||
<link rel="stylesheet" href="/style.css">
|
||
</head>
|
||
<body>
|
||
|
||
<!-- Circuit-trace ornament, tiled once behind everything. -->
|
||
<svg class="filigree" aria-hidden="true">
|
||
<defs>
|
||
<pattern id="orn" width="60" height="52" patternUnits="userSpaceOnUse">
|
||
<g fill="none" stroke="currentColor" stroke-width="0.7">
|
||
<path d="M15 0 L45 0 L60 26 L45 52 L15 52 L0 26 Z"/>
|
||
<path d="M30 26 L60 26 M30 26 L15 0 M30 26 L15 52"/>
|
||
<circle cx="30" cy="26" r="1.6"/>
|
||
<circle cx="0" cy="26" r="1.2"/>
|
||
<circle cx="60" cy="26" r="1.2"/>
|
||
</g>
|
||
</pattern>
|
||
</defs>
|
||
<rect width="100%" height="100%" fill="url(#orn)"/>
|
||
</svg>
|
||
|
||
<header class="topbar">
|
||
<div class="brand">QUANTUM<span>ARCADE</span></div>
|
||
<div class="balance" id="balance-wrap" hidden>
|
||
<span class="label">balance</span>
|
||
<span class="value" id="balance">—</span>
|
||
</div>
|
||
<button class="sound" id="sound-toggle" title="Ambient sound">♪</button>
|
||
</header>
|
||
|
||
<!-- Sign-in -->
|
||
<section class="panel center" id="signin">
|
||
<h1>ACCESS TERMINAL</h1>
|
||
<p class="muted small">
|
||
No account. No email. No password. This device generates a keypair
|
||
that <em>is</em> your identity.
|
||
</p>
|
||
<input id="nickname" maxlength="20" placeholder="handle" autocomplete="off">
|
||
<button class="primary" id="enter">Connect</button>
|
||
<p class="fineprint" id="keynote"></p>
|
||
<div class="cryptobadge">
|
||
<span>Provably fair</span>
|
||
<span>SHA-256 commit-reveal</span>
|
||
<span>AGPL-3.0</span>
|
||
</div>
|
||
</section>
|
||
|
||
<main id="app" hidden>
|
||
|
||
<!-- ============ CRASH ============ -->
|
||
<section class="view" id="view-crash">
|
||
<div class="gamepick" id="gamepick"></div>
|
||
|
||
<div class="stage">
|
||
<canvas id="scene"></canvas>
|
||
<div class="readout">
|
||
<div class="multiplier" id="multiplier">1.00×</div>
|
||
<div class="state" id="state">connecting…</div>
|
||
</div>
|
||
<div class="stagetop">
|
||
<span class="pot" id="potline"></span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Live crash history strip, directly under the stage. -->
|
||
<div class="strip" id="strip"></div>
|
||
|
||
<div class="controls">
|
||
<div class="stakerow">
|
||
<button class="chip" data-stake="1000">1</button>
|
||
<button class="chip" data-stake="5000">5</button>
|
||
<button class="chip" data-stake="25000">25</button>
|
||
<button class="chip" data-stake="100000">100</button>
|
||
<span class="unit">sats</span>
|
||
</div>
|
||
|
||
<div class="autorow" id="autorow">
|
||
<label for="auto-target">auto out</label>
|
||
<input id="auto-target" type="number" min="1.01" step="0.1"
|
||
inputmode="decimal" placeholder="off">
|
||
<span class="x">×</span>
|
||
<div class="presets">
|
||
<button data-auto="1.5">1.5</button>
|
||
<button data-auto="2">2</button>
|
||
<button data-auto="5">5</button>
|
||
</div>
|
||
</div>
|
||
|
||
<button class="primary big" id="action">Place bet</button>
|
||
<div class="hint" id="hint"></div>
|
||
</div>
|
||
|
||
<div class="players" id="players"></div>
|
||
|
||
<details class="proof">
|
||
<summary>Fairness for this round</summary>
|
||
<div class="kv"><span>commitment</span><code id="commitment">—</code></div>
|
||
<div class="kv"><span>revealed seed</span><code id="revealed">sealed until the round ends</code></div>
|
||
<p class="muted small">
|
||
The commitment is published before betting opens. The crash point comes
|
||
from that seed combined with every player's key — so it cannot be
|
||
chosen after seeing who joined.
|
||
</p>
|
||
</details>
|
||
</section>
|
||
|
||
<!-- ============ SCRATCH ============ -->
|
||
<section class="view" id="view-scratch" hidden>
|
||
<div id="tickets"></div>
|
||
</section>
|
||
|
||
<!-- ============ PORTFOLIO ============ -->
|
||
<section class="view" id="view-portfolio" hidden>
|
||
|
||
<div class="tiles">
|
||
<div class="tile">
|
||
<span class="tile-label">balance</span>
|
||
<span class="tile-value" id="t-balance">—</span>
|
||
<div class="tile-spark" id="spark-balance"></div>
|
||
</div>
|
||
<div class="tile">
|
||
<span class="tile-label">session</span>
|
||
<span class="tile-value" id="t-session">—</span>
|
||
<span class="tile-sub" id="t-session-sub">since you connected</span>
|
||
</div>
|
||
<div class="tile">
|
||
<span class="tile-label">wagered</span>
|
||
<span class="tile-value" id="t-wagered">—</span>
|
||
<span class="tile-sub" id="t-plays">0 plays</span>
|
||
</div>
|
||
<div class="tile">
|
||
<span class="tile-label">best hit</span>
|
||
<span class="tile-value" id="t-best">—</span>
|
||
<span class="tile-sub">largest single win</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>Balance over time</h2>
|
||
<div class="chart" id="chart-balance"></div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>Cash-out rate</h2>
|
||
<div class="chart chart-donut" id="chart-donut"></div>
|
||
<p class="muted small" id="donut-note"></p>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>Where rounds ended</h2>
|
||
<div class="chart" id="chart-dist"></div>
|
||
<p class="muted small">
|
||
Bars are what actually happened. The dashed line is what the published
|
||
maths predicts. They should converge — that is the point.
|
||
</p>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>Recent rounds</h2>
|
||
<div class="chart" id="chart-history"></div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ============ WALLET ============ -->
|
||
<section class="view" id="view-wallet" hidden>
|
||
<div class="card">
|
||
<h2>Your key</h2>
|
||
<p class="muted small">Share this so friends can send you sats.</p>
|
||
<code class="pubkey" id="pubkey">—</code>
|
||
<button id="copykey">Copy</button>
|
||
</div>
|
||
<!-- Lightning in. Hidden unless the server has a node configured, so a
|
||
play-money deployment does not advertise a deposit it cannot take. -->
|
||
<div class="card" id="card-deposit" hidden>
|
||
<h2>Add sats</h2>
|
||
<div class="stakerow">
|
||
<button class="chip" data-dep="1000">1k</button>
|
||
<button class="chip" data-dep="5000">5k</button>
|
||
<button class="chip" data-dep="25000">25k</button>
|
||
<input id="dep-amt" type="number" min="1" inputmode="numeric" placeholder="sats">
|
||
</div>
|
||
<button class="primary" id="do-deposit">Create invoice</button>
|
||
<div class="hint" id="dep-hint"></div>
|
||
|
||
<div id="dep-invoice" hidden>
|
||
<div class="qrwrap" id="dep-qr"></div>
|
||
<code class="pubkey" id="dep-bolt11"></code>
|
||
<div class="stakerow">
|
||
<button id="dep-copy">Copy invoice</button>
|
||
<button id="dep-check">I have paid</button>
|
||
</div>
|
||
<p class="muted small" id="dep-status">
|
||
Scan with any Lightning wallet. Your balance updates once it settles.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Lightning out. -->
|
||
<div class="card" id="card-withdraw" hidden>
|
||
<h2>Cash out to Lightning</h2>
|
||
<p class="muted small">
|
||
Paste an invoice from your own wallet for the amount you want. Large
|
||
withdrawals are held for the operator to approve.
|
||
</p>
|
||
<input id="wd-bolt11" placeholder="lnbc… invoice" autocomplete="off">
|
||
<input id="wd-amt" type="number" min="1" inputmode="numeric"
|
||
placeholder="amount in sats">
|
||
<button class="primary" id="do-withdraw">Withdraw</button>
|
||
<div class="hint" id="wd-hint"></div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>Send sats</h2>
|
||
<input id="to-key" placeholder="recipient key" autocomplete="off">
|
||
<input id="send-amt" type="number" min="1" inputmode="numeric"
|
||
placeholder="amount in sats">
|
||
<button class="primary" id="send">Send</button>
|
||
<div class="hint" id="send-hint"></div>
|
||
</div>
|
||
<div class="card">
|
||
<h2>Every change to your balance</h2>
|
||
<div id="history" class="history"></div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ============ VERIFY ============ -->
|
||
<section class="view" id="view-verify" hidden>
|
||
<div class="card">
|
||
<h2>Check any round</h2>
|
||
<p class="muted small">
|
||
Enter a round number. Your phone recomputes the outcome from the
|
||
published seeds — it does not take the server's word for anything.
|
||
</p>
|
||
<input id="verify-id" type="number" inputmode="numeric" min="1"
|
||
placeholder="round number">
|
||
<button class="primary" id="do-verify">Verify</button>
|
||
<div id="verify-out" class="verify-out"></div>
|
||
</div>
|
||
<div class="card">
|
||
<h2>Underlying tech</h2>
|
||
<dl class="specs">
|
||
<dt>Identity</dt>
|
||
<dd>Ed25519 signed challenge, single-use and replay-proof.
|
||
Hybrid Ed25519 + ML-DSA-65 (FIPS 204) is implemented and tested
|
||
server-side; browser signing lands with the WASM module.</dd>
|
||
<dt>Transport</dt>
|
||
<dd>TLS 1.3 with X25519MLKEM768 hybrid key exchange when served over
|
||
HTTPS — post-quantum against harvest-now-decrypt-later</dd>
|
||
<dt>Fairness</dt>
|
||
<dd>SHA-256 commitment, HMAC-SHA256 outcome derivation. Hash-based,
|
||
so Grover only halves the margin: quantum-resistant as it stands.</dd>
|
||
<dt>Settlement</dt>
|
||
<dd>Bitcoin and Lightning sign with secp256k1, which is
|
||
<em>not</em> post-quantum. No application choice changes that.</dd>
|
||
<dt>Simulation</dt>
|
||
<dd>Q32.32 fixed-point, zero floating point, bit-identical replay</dd>
|
||
<dt>Ledger</dt>
|
||
<dd>Append-only double-entry, DB-enforced, audited every request</dd>
|
||
<dt>House edge</dt>
|
||
<dd>1.00% — verified by test across 2,000,000 simulated plays</dd>
|
||
<dt>Licence</dt>
|
||
<dd>AGPL-3.0 — every line auditable, forks must publish</dd>
|
||
</dl>
|
||
</div>
|
||
</section>
|
||
|
||
</main>
|
||
|
||
<!-- Bottom navigation: thumb-reachable, which is where navigation belongs on a
|
||
phone held one-handed at a party. -->
|
||
<nav class="tabs" id="tabs" hidden>
|
||
<button class="tab active" data-view="crash">
|
||
<span class="ico">▲</span><span>Crash</span>
|
||
</button>
|
||
<button class="tab" data-view="scratch">
|
||
<span class="ico">◈</span><span>Scratch</span>
|
||
</button>
|
||
<button class="tab" data-view="portfolio">
|
||
<span class="ico">▤</span><span>Stats</span>
|
||
</button>
|
||
<button class="tab" data-view="wallet">
|
||
<span class="ico">◉</span><span>Wallet</span>
|
||
</button>
|
||
<button class="tab" data-view="verify">
|
||
<span class="ico">✓</span><span>Verify</span>
|
||
</button>
|
||
</nav>
|
||
|
||
<script type="module" src="/app.js"></script>
|
||
</body>
|
||
</html>
|