Cashing out was the least approachable thing here: open your wallet, create an invoice for exactly the right amount, copy it, come back, paste it. That is the step people abandon, leaving sats behind. LNURL-withdraw replaces it with a scan. The arcade shows a code, the wallet pulls the funds, and the player never handles an invoice or types an amount. The paste path is kept for wallets without LNURL support, but folded away. The withdraw token is a bearer instrument, so it is random, single-use, bound to one account and one amount, and expires in five minutes. Sixteen goroutines racing one code yield exactly one payment. Funds are debited when the code is issued — otherwise a player could cash out and bet the same sats before the wallet claimed them — and a sweep refunds any code that is never scanned. bech32 is verified against the BIP-173 vectors, including the invalid ones. Getting this wrong produces codes that silently fail to scan with no useful error for the player. Adds a three-card first-run walkthrough, an explanation of what a multiplier target means, and a one-time confirmation before a player's first real-money action — the interface is deliberately frictionless, and that is the one place a moment of friction is worth it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
336 lines
12 KiB
HTML
336 lines
12 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>
|
||
<p class="explain" id="auto-explain">
|
||
Leave this off to cash out by tapping. Set it and you stop
|
||
automatically — 2× means you double your stake and get out.
|
||
</p>
|
||
|
||
<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. Scanning is the whole flow: no invoice to create, no
|
||
amount to type into a second app. -->
|
||
<div class="card" id="card-withdraw" hidden>
|
||
<h2>Cash out</h2>
|
||
<p class="muted small">
|
||
Pick an amount and scan the code with your Lightning wallet. Your
|
||
wallet pulls the sats — you never make an invoice.
|
||
</p>
|
||
<div class="stakerow">
|
||
<button class="chip" data-wd="1000">1k</button>
|
||
<button class="chip" data-wd="5000">5k</button>
|
||
<button class="chip" data-wd="all">All</button>
|
||
<input id="wd-amt" type="number" min="1" inputmode="numeric" placeholder="sats">
|
||
</div>
|
||
<button class="primary" id="do-withdraw">Show cash-out code</button>
|
||
<div class="hint" id="wd-hint"></div>
|
||
|
||
<div id="wd-code" hidden>
|
||
<div class="qrwrap" id="wd-qr"></div>
|
||
<p class="muted small center" id="wd-expiry"></p>
|
||
<button id="wd-copy">Copy code</button>
|
||
</div>
|
||
|
||
<details class="proof">
|
||
<summary>Paste an invoice instead</summary>
|
||
<p class="muted small">
|
||
If your wallet cannot scan LNURL, make an invoice for the amount and
|
||
paste it here.
|
||
</p>
|
||
<input id="wd-bolt11" placeholder="lnbc… invoice" autocomplete="off">
|
||
<button id="do-withdraw-manual">Withdraw to invoice</button>
|
||
</details>
|
||
</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>
|
||
|
||
<!-- First run only. Three cards, then it never appears again. -->
|
||
<div class="tour" id="tour" hidden>
|
||
<div class="tourcard">
|
||
<div class="tourstep" id="tour-step"></div>
|
||
<h2 id="tour-title"></h2>
|
||
<p id="tour-body"></p>
|
||
<div class="tourdots" id="tour-dots"></div>
|
||
<button class="primary" id="tour-next">Got it</button>
|
||
<button class="tourskip" id="tour-skip">Skip</button>
|
||
</div>
|
||
</div>
|
||
|
||
<script type="module" src="/app.js"></script>
|
||
</body>
|
||
</html>
|