Author SHA1 Message Date
recovery cdd5723977 recover-ord-pass 2026-10-05 03:54:15 +00:00
16 changed files with 56 additions and 405 deletions

No files matched your search

-13
View File
@@ -1,13 +0,0 @@
# Orgchain (branch `orgchain`)
Super Testnet restyled for lineage, served at https://lineage.yolopreneur.lol/orgchain/.
- Same lab and code as `main`. The differences are a skin and copy: `src/lineage.css` (loads after `style.css` and
points its tokens at lineage's), a rehearsal guide and lineage branding in `index.html`, the lineage favicon,
relative paths (`vite.config.js` `base: './'`, the recursive preview, license links), the theme following the system
setting under its own storage key, and Inscriboor downloads linking to supertestnet.lol (the extension only connects
there, so the local test wallet is the path here).
- Build: `npm ci && npm run build`, then copy `dist/` to `ventures/lineage/orgchain/` on `teahubxyz/time` `gh-pages`.
Headers (CSP and the rest, matching supertestnet.lol's) come from `ventures/lineage/_headers`, scoped to
`/orgchain/*`.
- Keep in step with `main`: rebase this branch onto `main`, rebuild, recopy.
+2 -9
View File
@@ -41,7 +41,7 @@ Wallet ownership is derived from the unspent reveal output’s script, not a dis
Collectible cards and details show metadata decoded from inscription witnesses. **Metadata is data; indexers are interpretation.** `MetadataIndex` reconstructs every inscription event from confirmed block raw transactions in chain order, followed by raw mempool transactions. Events include id, content type, status, height/block hash, metadata, presence flag and parent. No UI metadata sidecar is persisted. A top-level string `metadata.protocol` is the default discovery convention; other fields and values have no built-in semantics. The Indexes surface shows counts, discovered names and the ordered feed for a selected protocol. Collectible cards and details show metadata decoded from inscription witnesses. **Metadata is data; indexers are interpretation.** `MetadataIndex` reconstructs every inscription event from confirmed block raw transactions in chain order, followed by raw mempool transactions. Events include id, content type, status, height/block hash, metadata, presence flag and parent. No UI metadata sidecar is persisted. A top-level string `metadata.protocol` is the default discovery convention; other fields and values have no built-in semantics. The Indexes surface shows counts, discovered names and the ordered feed for a selected protocol.
The optional `event-counter` reducer derives counts and ordered ids from only the selected protocol's events, displayed separately. Reducers are trusted application code registered above the base index, never downloaded or executed from inscriptions. Adding a reducer requires a source-code change; this is a seam for future metaprotocol experiments, not a sidechain consensus engine. Native ord metaprotocol tag 7 is supported as an independent inscription field. It is authoritative for protocol grouping; `metadata.protocol` remains a compatibility fallback when tag 7 is absent. The optional `event-counter` reducer derives counts and ordered ids from only the selected protocol's events, displayed separately. Reducers are trusted application code registered above the base index, never downloaded or executed from inscriptions. Adding a reducer requires a source-code change; this is a seam for future metaprotocol experiments, not a sidechain consensus engine. Native ord tag 7 is a separate future compatibility feature and is not conflated with `metadata.protocol`.
The **LOCAL / IN-BROWSER** API explorer calls `LocalOrdAPI.request(path)` and displays/copies its `{status, contentType, body}` response. No HTTP request is made. Fly/the static server cannot read browser state. Available paths: The **LOCAL / IN-BROWSER** API explorer calls `LocalOrdAPI.request(path)` and displays/copies its `{status, contentType, body}` response. No HTTP request is made. Fly/the static server cannot read browser state. Available paths:
@@ -66,7 +66,7 @@ Sat identity is **synthetic-local-fifo**, not a Bitcoin ordinal number. Height `
Select **Reinscribe an owned sat** to spend and return its current output while appending an inscription to that same sat. The original body and id remain, stack positions start at zero and ordering survives mining/replay/spends. A new 1,000-sat output is also created by the fixed commit/reveal template; reinscription targets the carried sat, not that new output. Parent and reinscription selection are mutually exclusive. The reinscription target determines its pointer. Select **Reinscribe an owned sat** to spend and return its current output while appending an inscription to that same sat. The original body and id remain, stack positions start at zero and ordering survives mining/replay/spends. A new 1,000-sat output is also created by the fixed commit/reveal template; reinscription targets the carried sat, not that new output. Parent and reinscription selection are mutually exclusive. The reinscription target determines its pointer.
Advanced pointer tag 2 is an unsigned 64-bit zero-based position across all reveal outputs. Encoding is little-endian with trailing zero bytes omitted (zero is empty); a pointer beyond total output value falls back to the first output sat. There is **one inscription per reveal**. Multiple inscriptions on one sat come from separate reveals. Actual Bitcoin subsidy schedules, miner fee recycling, multiple envelopes/reveal, arbitrary DOM recursion and regtest/public backends are deferred. Advanced pointer tag 2 is an unsigned 64-bit zero-based position across all reveal outputs. Encoding is little-endian with trailing zero bytes omitted (zero is empty); a pointer beyond total output value falls back to the first output sat. There is **one inscription per reveal**. Multiple inscriptions on one sat come from separate reveals. Actual Bitcoin subsidy schedules, miner fee recycling, multiple envelopes/reveal, arbitrary DOM recursion, native tag 7 and regtest/public backends are deferred.
## Mining at your pace ## Mining at your pace
@@ -139,10 +139,3 @@ Configuration references: [Fly app configuration](https://docs.fly.io/reference/
## Attribution and licenses ## Attribution and licenses
See [PROVENANCE.md](PROVENANCE.md) for upstream commit pins, exactly what was adapted, what was only inspiration, and the node_faker decision. License texts ship in `public/licenses/` and the app links them. No upstream UI, network relay, mainnet API or remote script loader was imported. See [PROVENANCE.md](PROVENANCE.md) for upstream commit pins, exactly what was adapted, what was only inspiration, and the node_faker decision. License texts ship in `public/licenses/` and the app links them. No upstream UI, network relay, mainnet API or remote script loader was imported.
## Prod (2026-10-14)
https://supertestnet.lol is served by Teahub pages from this repository's `pages` branch: the built `dist/` plus a
`_headers` file carrying the same security headers `server.js` sends. `fly.toml` and `Dockerfile` describe the
earlier Fly deployment and are kept for reference. History before 2026-10-14 is `matcha/supertestnet` from the old
factory; the extension is `supertestnet/inscriboor`.
+17 -27
View File
@@ -2,39 +2,29 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"> <meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="theme-color" content="#141414"> <meta name="theme-color" content="#f7931a">
<meta name="description" content="Super Testnet in lineage's clothes: rehearse an org's lineage (root, births, badges) as inscriptions on a private toy Bitcoin chain in your browser, with pretend money."> <meta name="description" content="A private Bitcoin playground. Mine fake coins, inscribe a thought, and follow it into a block.">
<title>Orgchain — rehearse an org on a small chain · Lineage</title> <title>Super Testnet — your own little Bitcoin universe</title>
<link rel="icon" type="image/svg+xml" href="favicon.svg"> <link rel="icon" type="image/svg+xml" href="/favicon.svg">
</head> </head>
<body> <body>
<header class="band"> <header>
<div class="band-in"> <a class="brand" href="#"><span class="mark" aria-hidden="true"></span> SUPER TESTNET <span class="version">v0</span></a>
<div> <div class="header-actions"><span class="network"><i></i> PRIVATE LOCAL CHAIN</span><button id="theme-toggle" class="theme-toggle quiet" type="button" aria-label="Toggle color theme">☀ Light</button></div>
<h1 class="wordmark">Orgchain</h1>
<p class="lede"><b>Rehearse an org on a small chain.</b> Mint a root, give birth to children, award a badge, all as inscriptions on a private toy Bitcoin chain in this tab, with pretend money.</p>
</div>
<div class="flags">
<span class="flag hz">Test coins</span>
<span class="flag">Private chain</span>
<a class="flag" href="../">Lineage ↗</a>
<button id="theme-toggle" class="flag" type="button" aria-label="Toggle color theme">☾ Dark</button>
</div>
</div>
</header> </header>
<main> <main>
<section class="rehearse blk" aria-labelledby="t-rehearse"><h2 class="tab" id="t-rehearse">Rehearsal <span class="sub">a lineage in four moves</span></h2><div class="panel"><ol class="moves"><li><b>Root.</b> Fund a wallet, then inscribe the org with metaprotocol <code>lineage/v0</code> and metadata like <code>{"lineage":"root","name":"yolopreneur"}</code>.</li><li><b>Birth.</b> Inscribe a child with the root as its parent. Only the parent's holder can: the parent is spent to make the child, and the child carries the parent's ID on-chain (ord tag 3).</li><li><b>Generations.</b> Children can be parents too: <code>time</code> under the root, <code>docscope</code> under <code>time</code>, as deep as the tree goes.</li><li><b>Badge.</b> Inscribe a badge once, like <code>{"lineage":"badge","skill":"launch","belt":"green"}</code>, then mint each award as a child of it with the badge as delegate, so every award shows the same mark.</li></ol><p class="later"><b>Not in this lab yet:</b> handing an award to a seat, graduation (moving a venture's inscription to its own new root) and closing (burning a parent so no more children can be born). Those need transfers and burns, which this lab doesn't do.</p></div></section> <section class="hero"><div><p class="eyebrow">BITCOIN, WITH ROOM TO EXPERIMENT</p><h1>Your own little<br>Bitcoin universe.</h1><p class="intro">Mine a block. Inscribe a thought. See it land in your wallet.<br>All in your browser, all with pretend money.</p></div><div class="orbit" aria-hidden="true"><div class="cube"><svg viewBox="0 0 100 100" width="82" height="82" fill="none" stroke="currentColor" stroke-width="5" aria-hidden="true"><path d="M35 17v12m12-12v12M35 76v12m12-12v12M24 29h25c22 0 22 23 0 23H24m0 0h28c22 0 22 24 0 24H24m7-47v47"/></svg></div><span>YOUR CHAIN. YOUR PACE.</span></div></section>
<aside class="notice"><strong>Test coins only.</strong> This is a private simulation, not Bitcoin testnet or mainnet. Local-wallet keys stay in this browser, unencrypted. Inscriboor keys stay in the extension. Never load real keys or send real funds here. Nothing on Orgchain is a real org, key or badge.</aside> <aside class="notice"><strong>Test coins only.</strong> This is a private simulation, not Bitcoin testnet or mainnet. Keys stay in this browser, unencrypted. Never load real keys or send real funds here.</aside>
<div id="status" role="status" aria-live="polite">Choose Inscriboor or a local test wallet to start your first loop.</div> <div id="status" role="status" aria-live="polite">Create a test wallet to start your first loop.</div>
<section class="workspace"> <section class="workspace">
<article class="panel wallet"><div class="panel-title"><span class="step">01</span><h2>Your test wallet</h2><span id="wallet-state" class="tag">NOT CREATED</span></div> <article class="panel wallet"><div class="panel-title"><span class="step">01</span><h2>Your test wallet</h2><span id="wallet-state" class="tag">NOT CREATED</span></div>
<div id="empty-wallet"><p>One wallet. A whole chain to play with.</p><button id="create-wallet" class="primary">Use local test wallet</button> <button id="connect-inscriboor" class="quiet">Get Inscriboor ↓</button><p id="inscriboor-status" class="muted">Looking for the extension…</p><details class="install-help"><summary>Install Inscriboor</summary><p class="muted">Inscriboor 0.2.4 connects on supertestnet.lol only. On Orgchain, use the local test wallet.</p><ol><li>Download and unzip Inscriboor.</li><li>Open <code>chrome://extensions</code> (or Brave extensions).</li><li>Turn on Developer mode.</li><li>Choose <strong>Load unpacked</strong> and select the unzipped <code>extension</code> folder.</li><li>Refresh this page. The button above will change from <strong>Get Inscriboor</strong> to <strong>Connect Inscriboor</strong>.</li></ol></details></div> <div id="empty-wallet"><p>One wallet. A whole chain to play with.</p><button id="create-wallet" class="primary">Create test wallet <span>↗</span></button></div>
<div id="active-wallet" hidden><p class="label">FAKE BTC BALANCE</p><div class="balance"><span id="balance">0</span><small>BTC</small></div><p id="reserved" class="muted"></p><label for="address">Local regtest-format address</label><input id="address" readonly><button id="export-wallet" class="quiet">↓ Save test wallet</button></div> <div id="active-wallet" hidden><p class="label">FAKE BTC BALANCE</p><div class="balance"><span id="balance">0</span><small>BTC</small></div><p id="reserved" class="muted"></p><label for="address">Local regtest-format address</label><input id="address" readonly><button id="export-wallet" class="quiet">↓ Save test wallet</button></div>
<div class="wallet-footer"><label class="file-button" for="load-wallet">↑ Load test wallet or lab backup</label><input id="load-wallet" type="file" accept=".json,application/json"><p class="muted">Saved on this device. Local-wallet backups contain their test key; Inscriboor backups contain only a public descriptor. A lab backup also preserves the chain.</p></div> <div class="wallet-footer"><label class="file-button" for="load-wallet">↑ Load test wallet or lab backup</label><input id="load-wallet" type="file" accept=".json,application/json"><p class="muted">Saved on this device. A wallet backup contains its test key; a lab backup also preserves the chain.</p></div>
</article> </article>
<article class="panel"><div class="panel-title"><span class="step">02</span><h2>Make some blocks</h2></div><p>Skip the faucet queue. Every block gives you <strong>50 fake BTC</strong>, ready to use immediately.</p><div class="chain-stats"><div><span class="label">BLOCK HEIGHT</span><strong id="height">0</strong></div><div><span class="label">PENDING TXS</span><strong id="pending">0</strong></div></div><button id="mine" class="primary" disabled>Mine now <span>+</span></button><label for="miner-interval">Auto miner</label><select id="miner-interval"><option value="0">OFF</option><option value="10">10s</option><option value="30">30s</option><option value="60">60s</option></select><p id="miner-status" aria-live="polite">Auto mining OFF</p><p class="muted">Keep a lab tab open; browser throttling may delay blocks. Mining also confirms your pending inscriptions.</p></article> <article class="panel"><div class="panel-title"><span class="step">02</span><h2>Make some blocks</h2></div><p>Skip the faucet queue. Every block gives you <strong>50 fake BTC</strong>, ready to use immediately.</p><div class="chain-stats"><div><span class="label">BLOCK HEIGHT</span><strong id="height">0</strong></div><div><span class="label">PENDING TXS</span><strong id="pending">0</strong></div></div><button id="mine" class="primary" disabled>Mine now <span>+</span></button><label for="miner-interval">Auto miner</label><select id="miner-interval"><option value="0">OFF</option><option value="10">10s</option><option value="30">30s</option><option value="60">60s</option></select><p id="miner-status" aria-live="polite">Auto mining OFF</p><p class="muted">Keep a lab tab open; browser throttling may delay blocks. Mining also confirms your pending inscriptions.</p></article>
<article class="panel inscribe"><div class="panel-title"><span class="step">03</span><h2>Inscribe a root, a birth, a badge</h2><span class="tag">INSCRIPTION LAB</span></div><label for="mime-type">Content type</label><select id="mime-type"></select><label for="body-file">Or upload a local file (16 KiB maximum)</label><input id="body-file" type="file"><label for="parent">Parent inscription (optional, immutable)</label><select id="parent"><option value="">No parent</option></select><label for="delegate">Delegate inscription ID (optional, future IDs allowed)</label><input id="delegate" placeholder="64 hex characters + i0"><label for="reinscribe">Reinscribe an owned sat (optional)</label><select id="reinscribe"><option value="">New sat</option></select><label for="pointer">Advanced pointer (zero-based output sat position)</label><input id="pointer" inputmode="numeric" placeholder="Default first sat"><button id="recursive-demo" class="quiet">Use recursive demo</button><p class="muted">Recursive HTML/SVG scripts use local fetch and ord.render(text) in an isolated worker; ord.id is the requested inscription. No DOM or network access.</p><label for="payload">What will you put on your chain?</label><textarea id="payload" placeholder="yolopreneur — the root of the tree…" rows="3" disabled></textarea><label for="metadata">JSON metadata (optional, independent of body)</label><textarea id="metadata" rows="3" placeholder='{"lineage":"birth","name":"time"}'></textarea><p id="metadata-count" class="muted">0 / 2048 CBOR bytes · ord tag 5 · JSON is authoring format only</p><label for="metaprotocol">Metaprotocol (optional, independent field)</label><input id="metaprotocol" placeholder="e.g. lineage/v0 · ord tag 7"><p class="muted">Native ord metaprotocol identifier. Metadata and inscription data remain independent.</p><div class="compose-footer"><span id="byte-count" class="muted">0 / 16384 bytes</span><button id="review" disabled>Review inscription ↗</button></div> <article class="panel inscribe"><div class="panel-title"><span class="step">03</span><h2>Leave a little something</h2><span class="tag">INSCRIPTION LAB</span></div><label for="mime-type">Content type</label><select id="mime-type"></select><label for="body-file">Or upload a local file (16 KiB maximum)</label><input id="body-file" type="file"><label for="parent">Parent inscription (optional, immutable)</label><select id="parent"><option value="">No parent</option></select><label for="delegate">Delegate inscription ID (optional, future IDs allowed)</label><input id="delegate" placeholder="64 hex characters + i0"><label for="reinscribe">Reinscribe an owned sat (optional)</label><select id="reinscribe"><option value="">New sat</option></select><label for="pointer">Advanced pointer (zero-based output sat position)</label><input id="pointer" inputmode="numeric" placeholder="Default first sat"><button id="recursive-demo" class="quiet">Use recursive demo</button><p class="muted">Recursive HTML/SVG scripts use local fetch and ord.render(text) in an isolated worker; ord.id is the requested inscription. No DOM or network access.</p><label for="payload">What will you put on your chain?</label><textarea id="payload" placeholder="Hello, little Bitcoin universe…" rows="3" disabled></textarea><label for="metadata">JSON metadata (optional, independent of body)</label><textarea id="metadata" rows="3" placeholder='{"protocol":"my-experiment","note":"hello"}'></textarea><p id="metadata-count" class="muted">0 / 2048 CBOR bytes · ord tag 5 · JSON is authoring format only</p><div class="compose-footer"><span id="byte-count" class="muted">0 / 16384 bytes</span><button id="review" disabled>Review inscription ↗</button></div>
<div id="review-panel" hidden><h3>Review before signing</h3><div id="review-preview"></div><pre id="review-text"></pre><p id="review-parent"></p><pre id="review-metadata"></pre><p>Two transactions: commit + reveal. <strong>1,000 fake sats</strong> in fees; <strong>1,000 fake sats</strong> in a new wallet output. Any selected parent or reinscription output is also returned in full; reinscription appends to its existing sat stack.</p><p id="signing-note" class="muted">The next button asks your active signer to approve commit + reveal, then submits them to your private chain.</p><button id="sign" class="primary">Sign & inscribe</button> <button id="cancel-review" class="quiet">Cancel</button></div> <div id="review-panel" hidden><h3>Review before signing</h3><div id="review-preview"></div><pre id="review-text"></pre><p id="review-parent"></p><pre id="review-metadata"></pre><p>Two transactions: commit + reveal. <strong>1,000 fake sats</strong> in fees; <strong>1,000 fake sats</strong> in a new wallet output. Any selected parent or reinscription output is also returned in full; reinscription appends to its existing sat stack.</p><p class="muted">The next button signs both transactions locally with this test wallet and submits them to your private chain.</p><button id="sign" class="primary">Sign & inscribe</button> <button id="cancel-review" class="quiet">Cancel</button></div>
</article> </article>
</section> </section>
<section class="collection-section"><div class="section-title"><div><p class="eyebrow">04 / THE THINGS YOU MADE</p><h2>Your inscriptions <span id="collection-count">0</span></h2></div><span class="muted">Owned by this wallet</span></div><div id="collectibles" class="collectibles"><p class="empty">Your first inscription belongs here.<br><span>Fund your wallet, write some text, and make it yours.</span></p></div></section> <section class="collection-section"><div class="section-title"><div><p class="eyebrow">04 / THE THINGS YOU MADE</p><h2>Your inscriptions <span id="collection-count">0</span></h2></div><span class="muted">Owned by this wallet</span></div><div id="collectibles" class="collectibles"><p class="empty">Your first inscription belongs here.<br><span>Fund your wallet, write some text, and make it yours.</span></p></div></section>
@@ -50,7 +40,7 @@
<article id="inspector" class="panel inspector" hidden tabindex="-1"></article> <article id="inspector" class="panel inspector" hidden tabindex="-1"></article>
</section> </section>
<section class="panel" id="indexes"><h2>Indexes</h2> <section class="panel" id="indexes"><h2>Indexes</h2>
<p>Metadata is data; indexers are interpretation. Native metaprotocol tag 7 is authoritative; metadata.protocol remains a compatibility fallback. Raw events remain uninterpreted.</p> <p>Metadata is data; indexers are interpretation. Discovery uses the top-level metadata.protocol string. Raw events remain uninterpreted.</p>
<p id="index-stats"></p><label for="protocol">Discovered protocol names</label><select id="protocol"></select> <p id="index-stats"></p><label for="protocol">Discovered protocol names</label><select id="protocol"></select>
<p id="protocol-stats"></p><h3>Ordered raw events</h3><p class="muted">Confirmed blocks in chain order, then mempool order.</p><div id="protocol-events"></div> <p id="protocol-stats"></p><h3>Ordered raw events</h3><p class="muted">Confirmed blocks in chain order, then mempool order.</p><div id="protocol-events"></div>
<label for="reducer">Optional trusted local indexer</label><select id="reducer"><option value="">None</option><option value="event-counter">Example: event-counter</option></select> <label for="reducer">Optional trusted local indexer</label><select id="reducer"><option value="">None</option><option value="event-counter">Example: event-counter</option></select>
@@ -63,7 +53,7 @@
<button id="api-try">Try</button> <button id="api-copy" class="quiet">Copy response</button> <button id="api-try">Try</button> <button id="api-copy" class="quiet">Copy response</button>
<pre id="api-response" aria-live="polite">Choose a path and press Try.</pre> <pre id="api-response" aria-live="polite">Choose a path and press Try.</pre>
</section> </section>
<details class="about"><summary>Under the hood &amp; attribution</summary><p>Orgchain is <a href="https://supertestnet.lol/">Super Testnet</a> in lineage's clothes: the same lab and code, restyled, with a rehearsal guide on top. Its lab state lives in this browser, separate from supertestnet.lol's.</p><p>Real serialized Bitcoin transactions, Schnorr signatures and Taproot commit/reveal scripts, on a deliberately simplified local UTXO chain. No proof of work, Bitcoin consensus, peer network, public broadcast, or production custody. Synthetic local sats follow FIFO input-to-output ranges. Child creation and reinscription preserve the owned sat. Fees retire the input tail; this is not Bitcoin consensus.</p><p>Built from and inspired by <a href="https://github.com/supertestnet/testnet_generator">Supertestnet’s testnet_generator</a>, <a href="https://github.com/supertestnet/inscriptions-online">inscriptions-online</a>, and <a href="https://github.com/supertestnet/vanilla-js-browser-wallet">vanilla-js-browser-wallet</a>. <a href="https://github.com/supertestnet/node_faker">node_faker</a> was evaluated for future RPC compatibility.</p><p>Transaction library: <a href="https://github.com/cmdruid/tapscript">tapscript by Christopher Scott</a> (<a href="licenses/tapscript.txt">CC-BY-1.0</a>). <a href="licenses/supertestnet-CC0.txt">Supertestnet CC0 license</a> · <a href="licenses/noble-curves.txt">noble-curves license</a> · <a href="licenses/noble-hashes.txt">noble-hashes license</a>.</p></details> <details class="about"><summary>Under the hood & attribution</summary><p>Real serialized Bitcoin transactions, Schnorr signatures and Taproot commit/reveal scripts, on a deliberately simplified local UTXO chain. No proof of work, Bitcoin consensus, peer network, public broadcast, or production custody. Synthetic local sats follow FIFO input-to-output ranges. Child creation and reinscription preserve the owned sat. Fees retire the input tail; this is not Bitcoin consensus.</p><p>Built from and inspired by <a href="https://github.com/supertestnet/testnet_generator">Supertestnet’s testnet_generator</a>, <a href="https://github.com/supertestnet/inscriptions-online">inscriptions-online</a>, and <a href="https://github.com/supertestnet/vanilla-js-browser-wallet">vanilla-js-browser-wallet</a>. <a href="https://github.com/supertestnet/node_faker">node_faker</a> was evaluated for future RPC compatibility.</p><p>Transaction library: <a href="https://github.com/cmdruid/tapscript">tapscript by Christopher Scott</a> (<a href="/licenses/tapscript.txt">CC-BY-1.0</a>). <a href="/licenses/supertestnet-CC0.txt">Supertestnet CC0 license</a> · <a href="/licenses/noble-curves.txt">noble-curves license</a> · <a href="/licenses/noble-hashes.txt">noble-hashes license</a>.</p></details>
<footer><span>SMALL CHAIN. WHOLE FAMILY TREE.</span><span class="made">Part of <a href="../">Lineage</a> · <a href="https://yolopreneur.lol/">yolopreneur.lol</a> · based on <a href="https://supertestnet.lol/">Super Testnet</a></span><button id="reset" class="quiet danger">Reset local lab</button></footer> <footer><span>SMALL CHAIN. BIG POSSIBILITIES.</span><button id="reset" class="quiet danger">Reset local lab</button></footer>
</main><script type="module" src="/src/main.js"></script> </main><script type="module" src="/src/main.js"></script>
</body></html> </body></html>
+1 -1
View File
@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect width="16" height="16" fill="#ffc21a"/><path d="M5 3v10h7" stroke="#141414" stroke-width="2.6" fill="none"/></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 34 34"><rect width="34" height="34" rx="8" fill="#f7931a"/></svg>

Before

Width:  |  Height:  |  Size: 181 B

After

Width:  |  Height:  |  Size: 119 B

+1 -1
View File
@@ -1 +1 @@
<!doctype html><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'nonce-local-ord-preview'; worker-src blob:; style-src 'unsafe-inline'; connect-src 'none'; img-src data:; form-action 'none'; base-uri 'none'"><div id="markup"></div><pre id="recursive-output"></pre><script nonce="local-ord-preview" src="recursive-preview.js"></script> <!doctype html><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'nonce-local-ord-preview'; worker-src blob:; style-src 'unsafe-inline'; connect-src 'none'; img-src data:; form-action 'none'; base-uri 'none'"><div id="markup"></div><pre id="recursive-output"></pre><script nonce="local-ord-preview" src="/recursive-preview.js"></script>
+1 -1
View File
@@ -83,7 +83,7 @@ export class LocalChain {
} }
config = { pubkey: parsed.pubkey }; config = { pubkey: parsed.pubkey };
inscription = { id: `${txid}i0`, txid, commitTxid: input.txid, text: parsed.text, inscription = { id: `${txid}i0`, txid, commitTxid: input.txid, text: parsed.text,
bytes: Array.from(parsed.bytes), contentType: parsed.contentType, parent: parsed.parent, delegate: parsed.delegate, pointer: parsed.pointer, metaprotocol: parsed.metaprotocol, metadata: parsed.metadata, height, value: POSTAGE }; bytes: Array.from(parsed.bytes), contentType: parsed.contentType, parent: parsed.parent, delegate: parsed.delegate, pointer: parsed.pointer, metadata: parsed.metadata, height, value: POSTAGE };
} }
tx.vin.forEach((input, i) => { tx.vin.forEach((input, i) => {
let valid = false; let valid = false;
-70
View File
@@ -1,70 +0,0 @@
import { Script, Tap, Tx } from '@cmdcode/tapscript';
import { getVin, getVout } from '../upstream/primitives.js';
import { InscriptionAdapter, envelope, POSTAGE, FEE } from './inscription.js';
import { NETWORK } from './wallet.js';
const signingTx = tx => ({
hex: Tx.encode(tx).hex,
prevouts: tx.vin.map(input => ({
value: Number(input.prevout.value),
scriptPubKey: input.prevout.scriptPubKey,
})),
});
export async function inscribeWithProvider(chain, wallet, value, parentId = null, metadata = undefined, options = {}) {
const adapter = new InscriptionAdapter(chain, wallet);
const { coin, parent, reinscribe, ...payload } = adapter.prepare(value, parentId, metadata, options);
const carried = parent ?? reinscribe;
const carriedItem = reinscribe?.inscriptions.find(item => item.id === options.reinscribe);
const pointer = carriedItem ? chain.satLocation(carriedItem.sat).offset : options.pointer ?? (parent ? parent.value : undefined);
const script = envelope(wallet.pubkey, payload, parentId, metadata, { ...options, pointer });
const leaf = Tap.encodeScript(script);
const [tapkey, control] = Tap.getPubKey(wallet.pubkey, { target: leaf });
const commit = Tx.create({
vin: [getVin(coin.txid, coin.vout, coin.value, coin.scriptPubKey)],
vout: [
getVout(POSTAGE + FEE, ['OP_1', tapkey]),
getVout(coin.value - POSTAGE - 2 * FEE, wallet.script),
],
});
const commitId = Tx.util.getTxid(commit);
const reveal = Tx.create({
vin: [
...(carried ? [getVin(carried.txid, carried.vout, carried.value, carried.scriptPubKey)] : []),
getVin(commitId, 0, POSTAGE + FEE, ['OP_1', tapkey]),
],
vout: [
...(carried ? [getVout(carried.value, wallet.script)] : []),
getVout(POSTAGE, wallet.script),
],
});
const revealInput = carried ? 1 : 0;
const signatures = await wallet.signInscription({
network: NETWORK,
commit: signingTx(commit),
reveal: { ...signingTx(reveal), scriptInput: revealInput, leaf },
intent: {
contentType: payload.contentType,
size: payload.bytes.length,
metadata: metadata !== undefined,
metaprotocol: options.metaprotocol ?? null,
parent: parentId,
delegate: options.delegate ?? null,
pointer: pointer ?? 'automatic',
reinscribe: options.reinscribe ?? null,
postage: POSTAGE,
fees: FEE * 2,
},
});
commit.vin[0].witness = [signatures.commit];
reveal.vin[revealInput].witness = [signatures.revealScript, Script.encode(script, false).hex, control];
if (carried) {
if (!signatures.revealKey) throw new Error('Inscriboor did not return the carried-input signature.');
reveal.vin[0].witness = [signatures.revealKey];
}
chain.submitPackage([Tx.encode(commit).hex, Tx.encode(reveal).hex]);
return `${Tx.util.getTxid(reveal)}i0`;
}
+5 -15
View File
@@ -40,9 +40,7 @@ function parseParent(value) {
// Local extension: MIME bytes, 520-byte pushes, optional ord parent tag 3. // Local extension: MIME bytes, 520-byte pushes, optional ord parent tag 3.
export function envelope(pubkey, value, parent = null, metadata = undefined, options = {}) { export function envelope(pubkey, value, parent = null, metadata = undefined, options = {}) {
const { contentType, bytes } = body(value); const { contentType, bytes } = body(value);
const { delegate = null, pointer, metaprotocol = null } = options; const { delegate = null, pointer } = options;
const metaprotocolBytes = metaprotocol === null || metaprotocol === undefined || metaprotocol === "" ? null : new TextEncoder().encode(String(metaprotocol));
if (metaprotocolBytes && metaprotocolBytes.length > 520) throw new Error("Metaprotocol identifier must be 520 bytes or less.");
const metadataBytes = metadata === undefined ? undefined : encodeMetadata(metadata); const metadataBytes = metadata === undefined ? undefined : encodeMetadata(metadata);
const chunks = []; const chunks = [];
for (let i = 0; i < bytes.length; i += 520) chunks.push(hex(bytes.slice(i, i + 520))); for (let i = 0; i < bytes.length; i += 520) chunks.push(hex(bytes.slice(i, i + 520)));
@@ -59,8 +57,7 @@ export function envelope(pubkey, value, parent = null, metadata = undefined, opt
(pointer === undefined ? '' : push('02') + push(encodePointer(pointer))) + (pointer === undefined ? '' : push('02') + push(encodePointer(pointer))) +
(parent ? push('03') + push(serializeParent(parent)) : '') + (parent ? push('03') + push(serializeParent(parent)) : '') +
(metadataBytes === undefined ? '' : Array.from({ length: Math.ceil(metadataBytes.length / 520) }, (_, i) => (metadataBytes === undefined ? '' : Array.from({ length: Math.ceil(metadataBytes.length / 520) }, (_, i) =>
push('05') + push(hex(metadataBytes.slice(i * 520, (i + 1) * 520)))).join('')) + push('05') + push(hex(metadataBytes.slice(i * 520, (i + 1) * 520)))).join('')) + '00' + chunks.map(push).join('') + '68';
(metaprotocolBytes ? push('07') + push(hex(metaprotocolBytes)) : '') + '00' + chunks.map(push).join('') + '68';
} }
export function readEnvelope(witness) { export function readEnvelope(witness) {
if (!Array.isArray(witness) || witness.length !== 3) throw new Error('Unsupported script-path witness.'); if (!Array.isArray(witness) || witness.length !== 3) throw new Error('Unsupported script-path witness.');
@@ -75,25 +72,19 @@ export function readEnvelope(witness) {
if (typeof chunk !== 'string' || !/^(?:[0-9a-f]{2}){1,520}$/.test(chunk)) throw new Error('Invalid metadata push.'); if (typeof chunk !== 'string' || !/^(?:[0-9a-f]{2}){1,520}$/.test(chunk)) throw new Error('Invalid metadata push.');
metadataChunks.push(chunk); cursor += 2; metadataChunks.push(chunk); cursor += 2;
} }
const metadata = metadataChunks.length ? decodeMetadata(unhex(metadataChunks.join(''))) : undefined;
let metaprotocol = null;
if (parts[cursor] === '07') {
const raw = parts[cursor + 1];
if (typeof raw !== 'string' || !/^(?:[0-9a-f]{2}){1,520}$/.test(raw)) throw new Error('Invalid metaprotocol field.');
metaprotocol = new TextDecoder().decode(unhex(raw)); cursor += 2;
}
if (parts[cursor] !== 'OP_0') throw new Error('Unsupported ord tag or missing body separator.'); if (parts[cursor] !== 'OP_0') throw new Error('Unsupported ord tag or missing body separator.');
const metadata = metadataChunks.length ? decodeMetadata(unhex(metadataChunks.join(''))) : undefined;
const payload = parts.slice(cursor + 1, -1); const payload = parts.slice(cursor + 1, -1);
if (!payload.length || payload.some(p => typeof p !== 'string' || !/^(?:[0-9a-f]{2})+$/.test(p))) throw new Error('Invalid body envelope.'); if (!payload.length || payload.some(p => typeof p !== 'string' || !/^(?:[0-9a-f]{2})+$/.test(p))) throw new Error('Invalid body envelope.');
const contentType = new TextDecoder().decode(unhex(parts[6])); const contentType = new TextDecoder().decode(unhex(parts[6]));
const value = body({ contentType, bytes: unhex(payload.join('')) }); const value = body({ contentType, bytes: unhex(payload.join('')) });
const expected = envelope(parts[0], value, parent, metadata, { delegate, pointer, metaprotocol }); const expected = envelope(parts[0], value, parent, metadata, { delegate, pointer });
// Read original v0 text snapshots whose content-type tag used OP_1. // Read original v0 text snapshots whose content-type tag used OP_1.
const legacy = metadata === undefined && !parent && contentType === MIME && value.bytes.length <= 280 && const legacy = metadata === undefined && !parent && contentType === MIME && value.bytes.length <= 280 &&
Script.encode([parts[0], 'OP_CHECKSIG', 'OP_0', 'OP_IF', textHex('ord'), '01', textHex(MIME), 'OP_0', hex(value.bytes), 'OP_ENDIF'], false).hex; Script.encode([parts[0], 'OP_CHECKSIG', 'OP_0', 'OP_IF', textHex('ord'), '01', textHex(MIME), 'OP_0', hex(value.bytes), 'OP_ENDIF'], false).hex;
if (expected !== witness[1] && legacy !== witness[1]) throw new Error('Unsupported ord envelope.'); if (expected !== witness[1] && legacy !== witness[1]) throw new Error('Unsupported ord envelope.');
if (!/^[0-9a-f]{64}$/.test(parts[0])) throw new Error('Invalid inscription signing key.'); if (!/^[0-9a-f]{64}$/.test(parts[0])) throw new Error('Invalid inscription signing key.');
return { ...value, parent, delegate, pointer, metaprotocol, metadata, metadataBytes: metadataChunks.length ? unhex(metadataChunks.join('')) : undefined, text: new TextDecoder().decode(value.bytes), pubkey: parts[0] }; return { ...value, parent, delegate, pointer, metadata, metadataBytes: metadataChunks.length ? unhex(metadataChunks.join('')) : undefined, text: new TextDecoder().decode(value.bytes), pubkey: parts[0] };
} }
export class InscriptionAdapter { export class InscriptionAdapter {
constructor(chain, wallet) { this.chain = chain; this.wallet = wallet; } constructor(chain, wallet) { this.chain = chain; this.wallet = wallet; }
@@ -102,7 +93,6 @@ export class InscriptionAdapter {
if (metadata !== undefined) encodeMetadata(metadata); if (metadata !== undefined) encodeMetadata(metadata);
if (options.delegate) serializeParent(options.delegate); if (options.delegate) serializeParent(options.delegate);
if (options.pointer !== undefined) encodePointer(options.pointer); if (options.pointer !== undefined) encodePointer(options.pointer);
if (options.metaprotocol !== undefined && options.metaprotocol !== null && options.metaprotocol !== '' && new TextEncoder().encode(String(options.metaprotocol)).length > 520) throw new Error('Metaprotocol identifier must be 520 bytes or less.');
if (parentId && options.reinscribe) throw new Error('Choose parent or reinscription, not both.'); if (parentId && options.reinscribe) throw new Error('Choose parent or reinscription, not both.');
const coins = this.chain.coins(this.wallet.script); const coins = this.chain.coins(this.wallet.script);
const parent = parentId ? coins.find(c => c.inscriptions?.some(i => i.id === parentId)) : null; const parent = parentId ? coins.find(c => c.inscriptions?.some(i => i.id === parentId)) : null;
+4 -4
View File
@@ -3,7 +3,7 @@ import { Tx } from '@cmdcode/tapscript';
import { readEnvelope } from './inscription.js'; import { readEnvelope } from './inscription.js';
// Rebuild solely from serialized transactions, never collectible/UI sidecars. // Rebuild solely from serialized transactions, never collectible/UI sidecars.
// Native ord metaprotocol tag 7 is authoritative; metadata.protocol remains a compatibility fallback. // Discovery is a convention, not native ord metaprotocol tag 7 support.
export class MetadataIndex { export class MetadataIndex {
constructor(chain) { constructor(chain) {
this.events = []; this.events = [];
@@ -21,12 +21,12 @@ export class MetadataIndex {
const pointer = BigInt(item.pointer ?? 0), total = tx.vout.reduce((n, o) => n + Number(o.value), 0); const pointer = BigInt(item.pointer ?? 0), total = tx.vout.reduce((n, o) => n + Number(o.value), 0);
const sat = satAt(ranges.flat(), pointer < BigInt(total) ? Number(pointer) : 0); const sat = satAt(ranges.flat(), pointer < BigInt(total) ? Number(pointer) : 0);
const metadata = item.metadata; const metadata = item.metadata;
this.events.push({ id: `${Tx.util.getTxid(tx)}i0`, contentType: item.contentType, delegate: item.delegate, pointer: item.pointer, metaprotocol: item.metaprotocol, sat, this.events.push({ id: `${Tx.util.getTxid(tx)}i0`, contentType: item.contentType, delegate: item.delegate, pointer: item.pointer, sat,
reinscriptionPosition: this.events.filter(e => e.sat === sat).length, reinscriptionPosition: this.events.filter(e => e.sat === sat).length,
status: height === null ? 'pending' : 'confirmed', height, block, status: height === null ? 'pending' : 'confirmed', height, block,
metadata: metadata ?? null, hasMetadata: metadata !== undefined, parent: item.parent, metadata: metadata ?? null, hasMetadata: metadata !== undefined, parent: item.parent,
protocol: item.metaprotocol ?? (metadata && !Array.isArray(metadata) && typeof metadata === 'object' && protocol: metadata && !Array.isArray(metadata) && typeof metadata === 'object' &&
Object.hasOwn(metadata, 'protocol') && typeof metadata.protocol === 'string' ? metadata.protocol : null) }); Object.hasOwn(metadata, 'protocol') && typeof metadata.protocol === 'string' ? metadata.protocol : null });
}; };
for (const block of chain.blocks) for (const raw of block.raws) visit(raw, block.height, block.hash); for (const block of chain.blocks) for (const raw of block.raws) visit(raw, block.height, block.hash);
for (const raw of chain.mempool) visit(raw, null, null); for (const raw of chain.mempool) visit(raw, null, null);
+2 -35
View File
@@ -3,15 +3,14 @@ import { schnorr, secp256k1 } from '@noble/curves/secp256k1';
import { bytesToHex } from '@noble/curves/abstract/utils'; import { bytesToHex } from '@noble/curves/abstract/utils';
export const NETWORK = 'super-testnet-local-v0'; export const NETWORK = 'super-testnet-local-v0';
export class TestWallet { export class TestWallet {
constructor(secret) { constructor(secret) {
if (!/^[0-9a-f]{64}$/.test(secret) || !secp256k1.utils.isValidPrivateKey(secret)) { if (!/^[0-9a-f]{64}$/.test(secret) || !secp256k1.utils.isValidPrivateKey(secret)) {
throw new Error('Invalid test wallet key. Load a Super Testnet v0 backup.'); throw new Error('Invalid test wallet key. Load a Super Testnet v0 backup.');
} }
this.kind = 'test-wallet';
this.secret = secret; this.secret = secret;
this.pubkey = bytesToHex(schnorr.getPublicKey(secret)); this.pubkey = bytesToHex(schnorr.getPublicKey(secret));
// Untweaked key-path output, as in testnet_generator. Never a mainnet address.
this.address = Address.p2tr.fromPubKey(this.pubkey, 'regtest'); this.address = Address.p2tr.fromPubKey(this.pubkey, 'regtest');
this.script = ['OP_1', this.pubkey]; this.script = ['OP_1', this.pubkey];
} }
@@ -22,40 +21,8 @@ export class TestWallet {
} }
return new TestWallet(backup.secret); return new TestWallet(backup.secret);
} }
backup() { return { kind:'test-wallet', version:1, network:NETWORK, secret:this.secret }; } backup() { return { kind: 'test-wallet', version: 1, network: NETWORK, secret: this.secret }; }
sign(tx, extension, inputIndex = 0) { sign(tx, extension, inputIndex = 0) {
return Signer.taproot.sign(this.secret, tx, inputIndex, extension ? { extension } : {}).hex; return Signer.taproot.sign(this.secret, tx, inputIndex, extension ? { extension } : {}).hex;
} }
} }
export class ExtensionWallet {
constructor(account, provider = null) {
if (account?.network !== NETWORK || !/^[0-9a-f]{64}$/.test(account?.pubkey ?? '')) {
throw new Error('Inscriboor returned an incompatible Super Testnet account.');
}
const address = Address.p2tr.fromPubKey(account.pubkey, 'regtest');
if (account.address !== address) throw new Error('Inscriboor account address does not match its public key.');
this.kind = 'extension-wallet';
this.pubkey = account.pubkey;
this.address = address;
this.script = ['OP_1', this.pubkey];
this.provider = provider;
}
static load(backup) {
if (backup?.kind !== 'extension-wallet' || backup?.version !== 1 || backup?.network !== NETWORK) {
throw new Error('Unsupported Inscriboor wallet descriptor.');
}
return new ExtensionWallet(backup);
}
attach(provider) { this.provider = provider; return this; }
backup() { return { kind:'extension-wallet', version:1, network:NETWORK, pubkey:this.pubkey, address:this.address }; }
async signInscription(params) {
if (!this.provider?.request) throw new Error('Reconnect the Inscriboor extension before signing.');
return this.provider.request({ method:'ord_signInscription', params });
}
}
export function loadWalletBackup(backup) {
if (backup?.kind === 'extension-wallet') return ExtensionWallet.load(backup);
return TestWallet.load(backup);
}
-153
View File
@@ -1,153 +0,0 @@
/* Orgchain: Super Testnet in lineage's clothes.
Same lab, dressed as lineage's key-ceremony sheet: raw concrete, black rule, hazard yellow for the action and for
anything live, everything in the system monospace. Restyle only: loads after style.css and repoints its tokens. */
:root {
--concrete: #e4e3dd; --slab: #f6f5f1; --ink: #141414; --ink-2: #4a4944; --ink-3: #66655f;
--signal: #ffc21a; --on-signal: #141414; --ok: #1d7a3a; --focus: #2346ff;
--band: #141414; --band-fg: #f6f5f1; --band-muted: #b0afa8; --shadow: #141414;
--mono: ui-monospace, "SF Mono", "Cascadia Mono", "JetBrains Mono", Menlo, Consolas, "Liberation Mono", "DejaVu Sans Mono", monospace;
--display: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--t-xs: 0.75rem; --t-s: 0.8125rem; --t-m: 0.9375rem; --t-l: 1.25rem;
}
:root[data-theme="dark"] {
--concrete: #151514; --slab: #1e1e1c; --ink: #ecebe4; --ink-2: #b9b8b0; --ink-3: #94938b;
--ok: #62c784; --focus: #93a8ff; --band: #0b0b0a; --band-fg: #ecebe4; --band-muted: #9d9c95; --shadow: #4d4c47;
}
/* Super Testnet's own tokens, pointed at lineage's in both themes (style.css sets light values on [data-theme]) */
:root, :root[data-theme="light"], :root[data-theme="dark"] {
--orange: var(--signal); --orange-2: var(--signal);
--bg: var(--concrete); --panel: var(--slab); --panel-2: var(--concrete); --soft: var(--concrete);
--text: var(--ink); --muted: var(--ink-2); --line: var(--ink);
font-family: var(--mono);
}
:root, :root[data-theme="light"] { --danger: #b3261e; }
:root[data-theme="dark"] { --danger: #ff8a7f; }
* { border-radius: 0 !important; }
html, body { background: var(--concrete); }
body { font: 400 var(--t-m)/1.5 var(--mono); color: var(--ink); transition: none; }
::selection { background: var(--signal); color: var(--on-signal); }
a { color: var(--ink); text-decoration-thickness: 2px; text-underline-offset: 3px; }
a:hover { background: var(--signal); color: var(--on-signal); }
code { font-family: inherit; font-weight: 700; }
button:focus-visible, a:focus-visible, summary:focus-visible, .file-button:focus-visible,
input:focus-visible, select:focus-visible, textarea:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }
/* header slab, as on lineage's key tree */
header.band { max-width: none; margin: 0; padding: 0; display: block; border-bottom: 3px solid var(--ink); background: var(--band); color: var(--band-fg); }
.band-in {
max-width: 1240px; margin: 0 auto; padding-block: 22px 20px;
padding-left: max(16px, env(safe-area-inset-left, 0px)); padding-right: max(16px, env(safe-area-inset-right, 0px));
display: flex; flex-wrap: wrap; gap: 16px 32px; align-items: flex-end; justify-content: space-between;
}
.wordmark { margin: 0; color: var(--band-fg); font: 900 clamp(3.25rem, 11vw, 7rem)/0.82 var(--display); letter-spacing: -0.05em; text-transform: uppercase; }
.lede { margin: 12px 0 0; color: var(--band-muted); font-size: var(--t-s); max-width: 58ch; }
.lede b { color: var(--band-fg); }
.flags { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.flag, button.flag {
font: 700 var(--t-xs)/1 var(--mono); letter-spacing: 0.1em; text-transform: uppercase; padding: 7px 9px 6px;
border: 2px solid var(--band-fg); background: transparent; color: var(--band-fg); box-shadow: none; text-decoration: none;
}
.flag.hz { background: var(--signal); border-color: var(--signal); color: var(--on-signal); }
a.flag:hover, button.flag:hover:not(:disabled) { background: var(--band-fg); color: var(--band); border-color: var(--band-fg); transform: none; }
button.flag:active:not(:disabled) { transform: none; }
/* blocks with ink tabs */
.blk { display: flex; flex-direction: column; align-items: flex-start; min-width: 0; }
.blk > .panel { align-self: stretch; }
.tab {
margin: 0; background: var(--ink); color: var(--slab); font: 700 var(--t-xs)/1 var(--mono);
letter-spacing: 0.14em; text-transform: uppercase; padding: 8px 11px 7px; display: flex; gap: 12px; max-width: 100%;
}
.tab .sub { font-weight: 400; letter-spacing: 0.06em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rehearse { margin: 40px 0 0; }
.moves { margin: 0; padding: 0 0 0 1.6em; display: grid; gap: 10px; font-size: var(--t-s); line-height: 1.6; max-width: 96ch; }
.moves li::marker { font-weight: 700; }
.moves b, .panel .later b { color: var(--ink); }
.panel .later { margin: 16px 0 0; padding: 10px 12px; border: 2px dashed var(--ink); font-size: var(--t-s); color: var(--ink-2); max-width: 96ch; }
.notice { margin-top: 28px; background: transparent; border: 2px dashed var(--ink); padding: 10px 12px; font-size: var(--t-s); color: var(--ink-2); }
.notice strong { color: var(--ink); }
#status { color: var(--ink); font: 700 var(--t-s)/1.5 var(--mono); padding: 16px 0; }
#status::before { content: "\25B8 "; }
#status.error { color: var(--danger); }
/* panels: slabs with a hard ink shadow */
.panel { background: var(--slab); border: 3px solid var(--ink); box-shadow: 8px 8px 0 var(--shadow); padding: 22px; }
.workspace { gap: 28px; }
.panel-title { gap: 12px; margin-bottom: 18px; flex-wrap: wrap; }
.panel-title h2, #indexes > h2, #api-explorer > h2 { font: 700 var(--t-m)/1.3 var(--mono); letter-spacing: 0.04em; text-transform: uppercase; margin: 0; }
.step { background: var(--ink); color: var(--slab); border: 0; padding: 7px 9px 6px; font: 700 var(--t-xs)/1 var(--mono); letter-spacing: 0.1em; }
.tag { border: 2px solid var(--ink); color: var(--ink); font: 700 0.6875rem/1 var(--mono); letter-spacing: 0.1em; text-transform: uppercase; padding: 5px 7px 4px; }
.panel p { font-size: var(--t-s); color: var(--ink-2); }
.label, .chain-kicker { font: 700 var(--t-xs)/1.2 var(--mono) !important; letter-spacing: 0.12em; text-transform: uppercase; color: var(--ink-2); }
h3 { font: 700 var(--t-m)/1.3 var(--mono); text-transform: uppercase; letter-spacing: 0.04em; }
/* form */
label { font: 700 var(--t-xs)/1.3 var(--mono); letter-spacing: 0.06em; color: var(--ink-2); margin: 16px 0 6px; }
input, textarea, select { background: var(--concrete); border: 2px solid var(--ink); color: var(--ink); font: 400 var(--t-s)/1.4 var(--mono); padding: 9px 10px; }
input:focus, textarea:focus, select:focus { border-color: var(--ink); outline: 3px solid var(--focus); outline-offset: 2px; }
input[type=file] { border-style: dashed; background: transparent; }
.file-button { color: var(--ink); font-weight: 700; text-decoration-thickness: 2px; }
.wallet-footer { border-top: 2px dashed var(--ink-3); }
.install-help summary { cursor: pointer; font-weight: 700; }
/* buttons, as on the key tree */
button {
font: 700 var(--t-s)/1 var(--mono); letter-spacing: 0.06em; text-transform: uppercase; padding: 10px 12px 9px;
background: var(--slab); color: var(--ink); border: 2px solid var(--ink); box-shadow: 3px 3px 0 var(--shadow); transition: none;
}
button:hover:not(:disabled) { background: var(--concrete); border-color: var(--ink); transform: none; }
button:active:not(:disabled) { transform: translate(3px, 3px); box-shadow: none; }
button:disabled { opacity: 0.45; box-shadow: none; }
button.primary { background: var(--signal); color: var(--on-signal); border-color: var(--ink); }
button.primary:hover:not(:disabled) { background: var(--signal); filter: brightness(1.05); }
.quiet { background: transparent; font-size: var(--t-xs); box-shadow: 2px 2px 0 var(--shadow); }
button.danger, .danger { color: var(--danger); }
/* numbers */
.balance { font: 700 2.4rem/1 var(--mono); letter-spacing: -0.02em; }
.balance small { color: var(--ink-2); font: 700 var(--t-xs)/1 var(--mono); letter-spacing: 0.1em; }
.chain-stats strong { font: 700 2.5rem/1 var(--mono); }
pre { background: var(--concrete); border: 2px solid var(--ink); font: 400 var(--t-xs)/1.6 var(--mono); }
/* sections */
.section-title { margin: 52px 0 18px; }
.eyebrow { display: inline-block; background: var(--ink); color: var(--slab); padding: 8px 11px 7px; font: 700 var(--t-xs)/1 var(--mono); letter-spacing: 0.14em; margin: 0 0 10px; }
.section-title h2 { font: 900 clamp(1.6rem, 3.4vw, 2.4rem)/1 var(--display); letter-spacing: -0.03em; text-transform: uppercase; }
#collection-count { background: var(--signal); color: var(--on-signal); font: 700 var(--t-s)/1 var(--mono); padding: 5px 8px 4px; vertical-align: middle; }
.empty { border: 2px dashed var(--ink); color: var(--ink-2); font-size: var(--t-s); }
.collectible { background: var(--slab); border: 3px solid var(--ink); box-shadow: 6px 6px 0 var(--shadow); }
.collectible pre { font: 400 1rem/1.5 var(--mono); border: 0; background: transparent; }
.inscription-preview { border: 2px solid var(--ink); }
/* chain board: pending is live, so it wears the signal */
.chain-board { background: var(--slab); border: 3px solid var(--ink); box-shadow: 8px 8px 0 var(--shadow); }
.chain-kicker { color: var(--ink); }
.mempool-empty { border: 2px dashed var(--ink); color: var(--ink-2); }
.mempool-card { background: var(--signal); color: var(--on-signal); border: 2px solid var(--ink); text-transform: none; letter-spacing: 0; }
.mempool-card strong { color: var(--on-signal); }
.mempool-card span { color: var(--on-signal); }
.chain-divider { border-left: 3px solid var(--ink); }
.chain-divider span { color: var(--ink-2); font: 700 var(--t-xs)/1 var(--mono); letter-spacing: 0.1em; }
.block-card { background: var(--concrete); border: 2px solid var(--ink); text-transform: none; letter-spacing: 0; font-weight: 400; }
.block-card:before { border-top: 3px solid var(--ink); opacity: 1; }
.block-card .block-accent { color: var(--on-signal); background: var(--signal); display: inline-block; padding: 2px 5px; }
.inspector h2 { font: 700 var(--t-l)/1.25 var(--mono); }
#protocol-events article { border-bottom: 1px dashed var(--ink-3); }
#indexes, #api-explorer { margin-top: 2.5rem; }
/* about + footer */
.about { border-top: 3px solid var(--ink); color: var(--ink-2); font-size: var(--t-s); }
.about summary { color: var(--ink); font: 700 var(--t-xs)/1.4 var(--mono); letter-spacing: 0.1em; text-transform: uppercase; }
footer { border-top: 3px solid var(--ink); font: 700 var(--t-xs)/1.4 var(--mono); letter-spacing: 0.12em; color: var(--ink); flex-wrap: wrap; gap: 12px; }
footer .made { color: var(--ink-2); font-weight: 400; letter-spacing: 0.04em; }
@media (max-width: 700px) {
.band-in { padding-block: 18px 16px; }
.panel, .chain-board { box-shadow: 5px 5px 0 var(--shadow); padding: 18px; }
}
@media (prefers-reduced-motion: reduce) { button:active:not(:disabled) { transform: none; } }
/* long ids and JSON wrap instead of widening the page */
.moves, .moves code, .collectible, .inspector, #review-panel, #status { overflow-wrap: anywhere; }
+14 -42
View File
@@ -1,12 +1,10 @@
import { RECURSIVE_DEMO } from './recursion.js'; import { RECURSIVE_DEMO } from './recursion.js';
import './style.css'; import './style.css';
import './lineage.css';
import { encodeMetadata, parseMetadataJSON } from './adapters/metadata.js'; import { encodeMetadata, parseMetadataJSON } from './adapters/metadata.js';
import { MetadataIndex, deriveProtocol } from './adapters/metadata-index.js'; import { MetadataIndex, deriveProtocol } from './adapters/metadata-index.js';
import { LocalOrdAPI } from './adapters/local-ord-api.js'; import { LocalOrdAPI } from './adapters/local-ord-api.js';
import { mineLab, setMiner, minerStatus } from './miner.js'; import { mineLab, setMiner, minerStatus } from './miner.js';
import { TestWallet, ExtensionWallet, loadWalletBackup } from './adapters/wallet.js'; import { TestWallet } from './adapters/wallet.js';
import { inscribeWithProvider } from './adapters/extension-inscription.js';
import { LocalRPC } from './adapters/chain.js'; import { LocalRPC } from './adapters/chain.js';
import { InscriptionAdapter, MAX_BYTES, MIME_TYPES } from './adapters/inscription.js'; import { InscriptionAdapter, MAX_BYTES, MIME_TYPES } from './adapters/inscription.js';
import { preview, clearPreviews } from './preview.js'; import { preview, clearPreviews } from './preview.js';
@@ -15,11 +13,9 @@ import { STORAGE_KEY, readLab, mutateLab, decodeLab, encodeLab } from './storage
const $ = id => document.getElementById(id); const $ = id => document.getElementById(id);
const btc = sats => (sats / 1e8).toFixed(8); const btc = sats => (sats / 1e8).toFixed(8);
const short = id => `${id.slice(0, 12)}…${id.slice(-8)}`; const short = id => `${id.slice(0, 12)}…${id.slice(-8)}`;
const THEME_KEY = 'orgchain-theme'; const THEME_KEY = 'super-testnet-theme';
let fileBytes = null, fileLoading = false, fileRevision = 0; let fileBytes = null, fileLoading = false, fileRevision = 0;
let lab, busy = false, reviewed = null, failed = false; let lab, busy = false, reviewed = null, failed = false;
let inscriboorProvider = null;
const INSCRIBOOR_RELEASE = { version: '0.2.4', url: 'https://supertestnet.lol/downloads/inscriboor-extension-v0.2.4.zip' };
const message = (text, error = false) => { $('status').textContent = text; $('status').classList.toggle('error', error); }; const message = (text, error = false) => { $('status').textContent = text; $('status').classList.toggle('error', error); };
const el = (tag, text, className) => { const node = document.createElement(tag); if (text !== undefined) node.textContent = text; if (className) node.className = className; return node; }; const el = (tag, text, className) => { const node = document.createElement(tag); if (text !== undefined) node.textContent = text; if (className) node.className = className; return node; };
@@ -47,7 +43,7 @@ function setTheme(theme, persist = false) {
} }
function initTheme() { function initTheme() {
const saved = localStorage.getItem(THEME_KEY); const saved = localStorage.getItem(THEME_KEY);
setTheme(saved === 'light' || saved === 'dark' ? saved : (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')); setTheme(saved === 'light' || saved === 'dark' ? saved : 'dark');
} }
function download(name, contents) { function download(name, contents) {
const url = URL.createObjectURL(new Blob([contents], { type: 'application/json' })); const url = URL.createObjectURL(new Blob([contents], { type: 'application/json' }));
@@ -62,7 +58,7 @@ const composerBody = () => ({ contentType: $('mime-type').value, bytes: fileByte
function updateCount() { $('byte-count').textContent = `${composerBody().bytes.length} / ${MAX_BYTES} bytes`; } function updateCount() { $('byte-count').textContent = `${composerBody().bytes.length} / ${MAX_BYTES} bytes`; }
function resetComposer() { function resetComposer() {
fileRevision++; fileBytes = null; $('body-file').value = ''; $('payload').value = ''; fileRevision++; fileBytes = null; $('body-file').value = ''; $('payload').value = '';
$('delegate').value = ''; $('pointer').value = ''; $('reinscribe').value = ''; $('metaprotocol').value = ''; $('metadata').value = ''; updateMetadataCount(); $('parent').value = ''; closeReview(); updateCount(); $('delegate').value = ''; $('pointer').value = ''; $('reinscribe').value = ''; $('metadata').value = ''; updateMetadataCount(); $('parent').value = ''; closeReview(); updateCount();
} }
function lineage(item, chain) { function lineage(item, chain) {
const p = el('p', undefined, 'tx-links'); const p = el('p', undefined, 'tx-links');
@@ -90,7 +86,6 @@ function updateMetadataCount() {
function metadataView(item) { function metadataView(item) {
const node = el('div', undefined, 'metadata-document'); const node = el('div', undefined, 'metadata-document');
if (item.metadata !== undefined) node.append(el('strong', 'Decoded metadata'), el('pre', JSON.stringify(item.metadata, null, 2))); if (item.metadata !== undefined) node.append(el('strong', 'Decoded metadata'), el('pre', JSON.stringify(item.metadata, null, 2)));
if (item.metaprotocol) node.append(el('strong', `Metaprotocol: ${item.metaprotocol}`));
return node; return node;
} }
function renderIndexes(chain) { function renderIndexes(chain) {
@@ -170,17 +165,14 @@ function render() {
const wallet = lab?.wallet, chain = lab?.chain; const wallet = lab?.wallet, chain = lab?.chain;
const disabled = busy || failed; const disabled = busy || failed;
$('empty-wallet').hidden = !!wallet; $('active-wallet').hidden = !wallet; $('empty-wallet').hidden = !!wallet; $('active-wallet').hidden = !wallet;
$('wallet-state').textContent = wallet ? (wallet.kind === 'extension-wallet' ? 'INSCRIBOOR / TEST' : 'LOCAL / TEST ONLY') : 'NOT CREATED'; $('wallet-state').textContent = wallet ? 'LOCAL / TEST ONLY' : 'NOT CREATED';
$('create-wallet').disabled = disabled; $('connect-inscriboor').disabled = disabled; $('load-wallet').disabled = disabled; $('create-wallet').disabled = disabled; $('load-wallet').disabled = disabled;
const inscriboorNeedsUpdate = !!inscriboorProvider && inscriboorProvider.version !== INSCRIBOOR_RELEASE.version;
$('connect-inscriboor').textContent = inscriboorNeedsUpdate ? 'Update Inscriboor ↓' : inscriboorProvider ? 'Connect Inscriboor ↗' : 'Get Inscriboor ↓';
$('inscriboor-status').textContent = inscriboorNeedsUpdate ? `Inscriboor ${inscriboorProvider.version ?? 'older build'} detected · ${INSCRIBOOR_RELEASE.version} available.` : inscriboorProvider ? `Inscriboor ${inscriboorProvider.version ?? ''} detected · extension key stays outside this page.` : 'Inscriboor not detected. Install it, refresh, then connect.';
$('mine').disabled = disabled || !wallet; $('mine').disabled = disabled || !wallet;
$('miner-interval').disabled = disabled || !wallet; $('miner-interval').disabled = disabled || !wallet;
$('miner-interval').value = String(lab?.miner?.interval ?? 0); $('miner-interval').value = String(lab?.miner?.interval ?? 0);
$('miner-status').textContent = minerStatus(lab); $('miner-status').textContent = minerStatus(lab);
$('payload').disabled = disabled || !wallet || !!fileBytes || fileLoading || $('mime-type').value.startsWith('image/') && $('mime-type').value !== 'image/svg+xml'; $('payload').disabled = disabled || !wallet || !!fileBytes || fileLoading || $('mime-type').value.startsWith('image/') && $('mime-type').value !== 'image/svg+xml';
for (const id of ['mime-type', 'body-file', 'parent', 'metadata', 'metaprotocol', 'delegate', 'pointer', 'reinscribe', 'recursive-demo']) $(id).disabled = disabled || !wallet || fileLoading; for (const id of ['mime-type', 'body-file', 'parent', 'metadata', 'delegate', 'pointer', 'reinscribe', 'recursive-demo']) $(id).disabled = disabled || !wallet || fileLoading;
$('review').disabled = disabled || !wallet || !chain?.coins(wallet.script).some(c => !c.inscription) || fileLoading || !composerBody().bytes.length; $('review').disabled = disabled || !wallet || !chain?.coins(wallet.script).some(c => !c.inscription) || fileLoading || !composerBody().bytes.length;
$('sign').disabled = disabled; $('sign').disabled = disabled;
$('export-wallet').disabled = disabled; $('export-lab').disabled = busy; $('export-wallet').disabled = disabled; $('export-lab').disabled = busy;
@@ -191,8 +183,6 @@ function render() {
$('balance').textContent = btc(chain.balance(wallet.script)); $('balance').textContent = btc(chain.balance(wallet.script));
const reserved = chain.coins(wallet.script).filter(c => c.inscription).reduce((n,c) => n+c.value,0); const reserved = chain.coins(wallet.script).filter(c => c.inscription).reduce((n,c) => n+c.value,0);
$('reserved').textContent = `${reserved.toLocaleString()} sats protected in collectibles · balance includes pending outputs`; $('reserved').textContent = `${reserved.toLocaleString()} sats protected in collectibles · balance includes pending outputs`;
$('export-wallet').textContent = wallet.kind === 'extension-wallet' ? 'Save wallet descriptor' : 'Save test wallet';
$('signing-note').textContent = wallet.kind === 'extension-wallet' ? 'The next button opens Inscriboor for approval. The page never receives the extension key.' : 'The next button signs locally with this browser test wallet and submits commit + reveal.';
} }
const items = wallet ? new InscriptionAdapter(chain, wallet).list() : []; const items = wallet ? new InscriptionAdapter(chain, wallet).list() : [];
const selectedParent = $('parent').value; const selectedParent = $('parent').value;
@@ -241,17 +231,6 @@ $('create-wallet').onclick = () => mutate(next => {
if (next.wallet) throw new Error('A wallet already exists in this browser. Reload to use it.'); if (next.wallet) throw new Error('A wallet already exists in this browser. Reload to use it.');
next.wallet = TestWallet.create(); next.wallet = TestWallet.create();
}, 'Test wallet created. Next: mine a block to get 50 fake BTC.'); }, 'Test wallet created. Next: mine a block to get 50 fake BTC.');
$('connect-inscriboor').onclick = async () => {
try {
const provider = inscriboorProvider ?? window.inscriboor;
if (!provider?.request || provider.version !== INSCRIBOOR_RELEASE.version) { location.href = INSCRIBOOR_RELEASE.url; return; }
const account = (await provider.request({ method:'bitcoin_getAccounts' }))?.[0];
await mutate(next => {
if (next.wallet) throw new Error('A wallet already exists in this browser. Reset or restore a different lab first.');
next.wallet = new ExtensionWallet(account, provider);
}, 'Inscriboor connected. Mine a block, prepare an inscription, then approve it in the extension.');
} catch (error) { message(error.message, true); }
};
const mine = automatic => mutate(next => mineLab(next, automatic), 'Block mined. Pending transactions moved from the mempool into your newest block.'); const mine = automatic => mutate(next => mineLab(next, automatic), 'Block mined. Pending transactions moved from the mempool into your newest block.');
$('mine').onclick = () => mine(false); $('mine').onclick = () => mine(false);
$('miner-interval').onchange = () => { $('miner-interval').onchange = () => {
@@ -266,7 +245,7 @@ $('metadata').oninput = () => { closeReview(); updateMetadataCount(); };
MIME_TYPES.forEach(type => $('mime-type').append(new Option(type, type))); MIME_TYPES.forEach(type => $('mime-type').append(new Option(type, type)));
$('payload').oninput = () => { closeReview(); updateCount(); render(); }; $('payload').oninput = () => { closeReview(); updateCount(); render(); };
$('parent').onchange = $('reinscribe').onchange = closeReview; $('parent').onchange = $('reinscribe').onchange = closeReview;
$('delegate').oninput = $('pointer').oninput = $('metaprotocol').oninput = closeReview; $('delegate').oninput = $('pointer').oninput = closeReview;
$('recursive-demo').onclick = () => { $('recursive-demo').onclick = () => {
fileRevision++; fileBytes = null; $('body-file').value = ''; $('mime-type').value = 'text/html'; fileRevision++; fileBytes = null; $('body-file').value = ''; $('mime-type').value = 'text/html';
$('payload').value = RECURSIVE_DEMO; closeReview(); updateCount(); render(); $('payload').value = RECURSIVE_DEMO; closeReview(); updateCount(); render();
@@ -292,14 +271,14 @@ $('body-file').onchange = async event => {
$('review').onclick = () => { $('review').onclick = () => {
try { try {
const payload = composerBody(), parent = $('parent').value || null, metadata = parseMetadataJSON($('metadata').value); const payload = composerBody(), parent = $('parent').value || null, metadata = parseMetadataJSON($('metadata').value);
const options = { delegate: $('delegate').value.trim() || null, metaprotocol: $('metaprotocol').value.trim() || null, reinscribe: $('reinscribe').value || null, ...($('pointer').value.trim() ? { pointer: $('pointer').value.trim() } : {}) }; const options = { delegate: $('delegate').value.trim() || null, reinscribe: $('reinscribe').value || null, ...($('pointer').value.trim() ? { pointer: $('pointer').value.trim() } : {}) };
new InscriptionAdapter(lab.chain, lab.wallet).prepare(payload, parent, metadata, options); new InscriptionAdapter(lab.chain, lab.wallet).prepare(payload, parent, metadata, options);
closeReview(); closeReview();
reviewed = { payload, parent, metadata, options, address: lab.wallet.address }; reviewed = { payload, parent, metadata, options, address: lab.wallet.address };
$('review-text').textContent = ['text/plain', 'application/json'].includes(payload.contentType) ? new TextDecoder().decode(payload.bytes) : `${payload.contentType} · ${payload.bytes.length} bytes`; $('review-text').textContent = ['text/plain', 'application/json'].includes(payload.contentType) ? new TextDecoder().decode(payload.bytes) : `${payload.contentType} · ${payload.bytes.length} bytes`;
if (!['text/plain', 'application/json'].includes(payload.contentType)) $('review-preview').append(preview(payload)); if (!['text/plain', 'application/json'].includes(payload.contentType)) $('review-preview').append(preview(payload));
$('review-parent').textContent = parent ? `Parent: ${parent}. Its output will be spent and its full value returned to this wallet. This relationship is immutable.` : 'No parent.'; $('review-parent').textContent = parent ? `Parent: ${parent}. Its output will be spent and its full value returned to this wallet. This relationship is immutable.` : 'No parent.';
$('review-parent').textContent += ` Delegate: ${options.delegate ?? 'none'}. Reinscribe: ${options.reinscribe ?? 'none'}. Pointer: ${options.pointer ?? 'automatic'}. Metaprotocol: ${options.metaprotocol ?? 'none'}.`; $('review-parent').textContent += ` Delegate: ${options.delegate ?? 'none'}. Reinscribe: ${options.reinscribe ?? 'none'}. Pointer: ${options.pointer ?? 'automatic'}.`;
$('review-metadata').textContent = metadata === undefined ? 'No metadata.' : `Metadata (CBOR, tag 5): ${JSON.stringify(metadata, null, 2)}`; $('review-metadata').textContent = metadata === undefined ? 'No metadata.' : `Metadata (CBOR, tag 5): ${JSON.stringify(metadata, null, 2)}`;
$('review-panel').hidden = false; $('review-panel').hidden = false;
$('sign').focus(); message('Review the payload and fake-sat costs. Nothing has been signed yet.'); $('sign').focus(); message('Review the payload and fake-sat costs. Nothing has been signed yet.');
@@ -309,14 +288,9 @@ $('cancel-review').onclick = closeReview;
$('sign').onclick = () => { $('sign').onclick = () => {
const consent = reviewed; const consent = reviewed;
if (!consent) return; if (!consent) return;
mutate(async next => { mutate(next => {
if (next.wallet?.address !== consent.address) throw new Error('Wallet changed. Review again before signing.'); if (next.wallet?.address !== consent.address) throw new Error('Wallet changed. Review again before signing.');
if (next.wallet.kind === 'extension-wallet') { new InscriptionAdapter(next.chain, next.wallet).inscribe(consent.payload, consent.parent, consent.metadata, consent.options);
const provider = inscriboorProvider ?? window.inscriboor;
if (!provider?.request) throw new Error('Reconnect the Inscriboor extension before signing.');
next.wallet.attach(provider);
await inscribeWithProvider(next.chain, next.wallet, consent.payload, consent.parent, consent.metadata, consent.options);
} else new InscriptionAdapter(next.chain, next.wallet).inscribe(consent.payload, consent.parent, consent.metadata, consent.options);
}, 'Inscription submitted. Its commit + reveal are now visible in the mempool; mine a block to confirm them.', resetComposer); }, 'Inscription submitted. Its commit + reveal are now visible in the mempool; mine a block to confirm them.', resetComposer);
}; };
$('export-wallet').onclick = () => download('super-testnet-wallet.json', JSON.stringify(lab.wallet.backup(), null, 2)); $('export-wallet').onclick = () => download('super-testnet-wallet.json', JSON.stringify(lab.wallet.backup(), null, 2));
@@ -331,7 +305,7 @@ $('load-wallet').onchange = async event => {
try { try {
const raw = await file.text(), data = JSON.parse(raw); const raw = await file.text(), data = JSON.parse(raw);
const restore = data.kind === 'test-lab'; const restore = data.kind === 'test-lab';
const loaded = restore ? decodeLab(raw) : loadWalletBackup(data); const loaded = restore ? decodeLab(raw) : TestWallet.load(data);
if (lab.wallet && !confirm(restore ? 'Replace this entire local lab with the backup? Save your current lab first.' : 'Switch the active test wallet? The local chain stays here. Save your current wallet first.')) return; if (lab.wallet && !confirm(restore ? 'Replace this entire local lab with the backup? Save your current lab first.' : 'Switch the active test wallet? The local chain stays here. Save your current wallet first.')) return;
await mutate(next => { await mutate(next => {
if (restore) Object.assign(next, loaded); else next.wallet = loaded; if (restore) Object.assign(next, loaded); else next.wallet = loaded;
@@ -407,11 +381,9 @@ window.addEventListener('storage', event => {
closeReview(); load(); if (!failed) message('Local lab changed in another tab; refreshed.'); closeReview(); load(); if (!failed) message('Local lab changed in another tab; refreshed.');
}); });
function load() { function load() {
try { lab = readLab(); failed = false; if (lab.wallet?.kind === 'extension-wallet' && inscriboorProvider) lab.wallet.attach(inscriboorProvider); if (lab.wallet) message(lab.wallet.kind === 'extension-wallet' ? 'Inscriboor wallet descriptor loaded. Reconnect the extension to sign.' : 'Test wallet loaded from this browser. Your local chain is ready.'); } try { lab = readLab(); failed = false; if (lab.wallet) message('Test wallet loaded from this browser. Your local chain is ready.'); }
catch (error) { failed = true; message(`Cannot load saved lab: ${error.message} Export it for recovery, then reset this local lab.`, true); } catch (error) { failed = true; message(`Cannot load saved lab: ${error.message} Export it for recovery, then reset this local lab.`, true); }
render(); render();
} }
window.addEventListener('inscriboor#initialized', () => { inscriboorProvider = window.inscriboor; if (lab?.wallet?.kind === 'extension-wallet') lab.wallet.attach(inscriboorProvider); render(); });
inscriboorProvider = window.inscriboor?.isInscriboor ? window.inscriboor : null;
initTheme(); initTheme();
load(); load();
+1 -1
View File
@@ -33,7 +33,7 @@ export function preview(item, api = null) {
node.title = `${type} recursive preview`; node.referrerPolicy = 'no-referrer'; node.title = `${type} recursive preview`; node.referrerPolicy = 'no-referrer';
const payload = recursivePayload(template.innerHTML, scripts, item.id); const payload = recursivePayload(template.innerHTML, scripts, item.id);
node.onload = () => { node.cleanup?.(); node.cleanup = connectRecursion(node, api, payload); }; node.onload = () => { node.cleanup?.(); node.cleanup = connectRecursion(node, api, payload); };
node.src = 'recursive-preview.html'; node.dataset.previewUrl = ''; node.className = 'inscription-preview'; node.src = '/recursive-preview.html'; node.dataset.previewUrl = ''; node.className = 'inscription-preview';
return node; return node;
} }
const csp = "default-src 'none'; style-src 'unsafe-inline'; script-src 'none'; form-action 'none'; base-uri 'none'"; const csp = "default-src 'none'; style-src 'unsafe-inline'; script-src 'none'; form-action 'none'; base-uri 'none'";
+8 -7
View File
@@ -1,16 +1,16 @@
import { minerPreference } from './miner.js'; import { minerPreference } from './miner.js';
import { LocalChain } from './adapters/chain.js'; import { LocalChain } from './adapters/chain.js';
import { loadWalletBackup, NETWORK } from './adapters/wallet.js'; import { TestWallet, NETWORK } from './adapters/wallet.js';
export const STORAGE_KEY = 'super-testnet:lab:v1'; export const STORAGE_KEY = 'super-testnet:lab:v1';
export function decodeLab(raw) { export function decodeLab(raw) {
if (!raw) return { wallet:null, chain:new LocalChain(), miner:minerPreference() }; if (!raw) return { wallet: null, chain: new LocalChain(), miner: minerPreference() };
const data = JSON.parse(raw); const data = JSON.parse(raw);
if (data.kind !== 'test-lab' || data.network !== NETWORK || data.version !== 1) throw new Error('Unsupported lab backup.'); if (data.kind !== 'test-lab' || data.network !== NETWORK || data.version !== 1) throw new Error('Unsupported lab backup.');
return { wallet:data.wallet ? loadWalletBackup(data.wallet) : null, chain:new LocalChain(data.chain), miner:minerPreference(data.miner) }; return { wallet: data.wallet ? TestWallet.load(data.wallet) : null, chain: new LocalChain(data.chain), miner: minerPreference(data.miner) };
} }
export const encodeLab = lab => JSON.stringify({ export const encodeLab = lab => JSON.stringify({
kind:'test-lab', network:NETWORK, version:1, kind: 'test-lab', network: NETWORK, version: 1,
wallet:lab.wallet?.backup() ?? null, chain:lab.chain.snapshot(), miner:minerPreference(lab.miner), wallet: lab.wallet?.backup() ?? null, chain: lab.chain.snapshot(), miner: minerPreference(lab.miner),
}); });
export const readLab = () => decodeLab(localStorage.getItem(STORAGE_KEY)); export const readLab = () => decodeLab(localStorage.getItem(STORAGE_KEY));
export function saveLab(lab) { export function saveLab(lab) {
@@ -18,12 +18,13 @@ export function saveLab(lab) {
catch { throw new Error('Browser storage is full or unavailable. No changes were saved. Export a lab backup before clearing storage.'); } catch { throw new Error('Browser storage is full or unavailable. No changes were saved. Export a lab backup before clearing storage.'); }
} }
export async function mutateLab(action, { locks=navigator.locks, read=readLab, save=saveLab } = {}) { // Shared by manual mining, auto mining, and all other lab mutations.
export async function mutateLab(action, { locks = navigator.locks, read = readLab, save = saveLab } = {}) {
if (!locks) throw new Error('This app needs a secure-context browser with Web Locks. Use localhost or HTTPS.'); if (!locks) throw new Error('This app needs a secure-context browser with Web Locks. Use localhost or HTTPS.');
return locks.request(STORAGE_KEY, async () => { return locks.request(STORAGE_KEY, async () => {
const candidate = read(); const candidate = read();
const changed = await action(candidate); const changed = await action(candidate);
if (changed !== false) save(candidate); if (changed !== false) save(candidate);
return { lab:candidate, changed:changed !== false }; return { lab: candidate, changed: changed !== false };
}); });
} }
-22
View File
@@ -453,25 +453,3 @@ test('multiple input ranges flow FIFO across outputs and protect inscribed fee t
assert.throws(()=>chain.submitPackage([Tx.encode(burn).hex]),/fee loss/); assert.throws(()=>chain.submitPackage([Tx.encode(burn).hex]),/fee loss/);
assert.deepEqual(chain.snapshot(),after); assert.ok(after.mempool.length>before.mempool.length); assert.deepEqual(chain.snapshot(),after); assert.ok(after.mempool.length>before.mempool.length);
}); });
test("native metaprotocol tag 7 round-trips and overrides metadata.protocol for tracking", () => {
const {ins,chain} = setup();
const id = ins.inscribe("hello", null, {protocol:"metadata-fallback", note:"kept"}, {metaprotocol:"tea-v1"});
const record = chain.transactions.get(id.slice(0,-2));
const witness = record.tx.vin.find(i => i.witness?.length === 3).witness;
const item = readEnvelope(witness);
assert.equal(item.metaprotocol, "tea-v1");
assert.deepEqual(item.metadata, {protocol:"metadata-fallback", note:"kept"});
const event = new MetadataIndex(chain).events.find(e => e.id === id);
assert.equal(event.metaprotocol, "tea-v1");
assert.equal(event.protocol, "tea-v1");
assert.deepEqual(new MetadataIndex(chain).forProtocol("tea-v1").map(e => e.id), [id]);
});
test("metadata.protocol remains a fallback when native metaprotocol is absent", () => {
const {ins,chain} = setup();
const id = ins.inscribe("legacy", null, {protocol:"legacy-protocol"});
const event = new MetadataIndex(chain).events.find(e => e.id === id);
assert.equal(event.metaprotocol, null);
assert.equal(event.protocol, "legacy-protocol");
});
-4
View File
@@ -1,4 +0,0 @@
import { defineConfig } from 'vite';
// Orgchain is served from lineage.yolopreneur.lol/orgchain/: relative asset URLs work at any sub-path.
export default defineConfig({ base: './' });