Created
April 20, 2026 01:39
-
-
Save wenakita/0beac860e1909deb7054995bd3383efa to your computer and use it in GitHub Desktop.
Zora sandbox probe — $PROBE coin source. Maps what an HTML content coin can actually do (window.ethereum? fetch? localStorage? signing?).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>Zora sandbox probe</title> | |
| <style> | |
| :root { color-scheme: dark; } | |
| html, body { | |
| margin: 0; padding: 0; background: #0a0a0a; color: #e5e5e5; | |
| font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, monospace; | |
| font-size: 12px; line-height: 1.55; | |
| min-height: 100vh; | |
| } | |
| .wrap { | |
| padding: 24px; max-width: 680px; margin: 0 auto; | |
| } | |
| h1 { | |
| font-size: 13px; letter-spacing: 0.12em; text-transform: uppercase; | |
| color: #8b8b8b; font-weight: 500; margin: 0 0 4px 0; | |
| } | |
| h2 { | |
| font-size: 18px; margin: 0 0 18px 0; color: #fff; font-weight: 500; | |
| font-family: ui-sans-serif, system-ui, -apple-system, sans-serif; | |
| } | |
| .row { | |
| display: flex; gap: 12px; align-items: baseline; | |
| padding: 10px 0; border-bottom: 1px solid #1a1a1a; | |
| } | |
| .row:last-child { border-bottom: 0; } | |
| .label { | |
| color: #888; min-width: 190px; flex-shrink: 0; | |
| } | |
| .value { | |
| color: #e5e5e5; word-break: break-all; flex: 1; | |
| } | |
| .tag { | |
| display: inline-block; padding: 1px 7px; border-radius: 3px; | |
| font-size: 10px; letter-spacing: 0.04em; text-transform: uppercase; | |
| margin-right: 6px; vertical-align: middle; | |
| } | |
| .tag.pass { background: #14321e; color: #6bd392; border: 1px solid #1f5b38; } | |
| .tag.fail { background: #3a1414; color: #ee6a6a; border: 1px solid #5c2020; } | |
| .tag.neutral { background: #1a1a1a; color: #999; border: 1px solid #262626; } | |
| .tag.pending { background: #1a1a1a; color: #c89a2b; border: 1px solid #3a2d13; } | |
| pre { | |
| background: #141414; padding: 10px; border: 1px solid #242424; | |
| border-radius: 4px; color: #cfcfcf; font-size: 11px; | |
| overflow-x: auto; margin: 6px 0 0 0; white-space: pre-wrap; | |
| } | |
| button { | |
| background: #fff; color: #000; border: 0; padding: 8px 14px; | |
| font-family: inherit; font-size: 11px; letter-spacing: 0.06em; | |
| text-transform: uppercase; cursor: pointer; border-radius: 4px; | |
| margin: 4px 6px 0 0; | |
| } | |
| button:hover { background: #e0e0e0; } | |
| button:disabled { opacity: 0.5; cursor: not-allowed; } | |
| button.secondary { background: transparent; color: #aaa; border: 1px solid #333; } | |
| button.secondary:hover { background: #181818; color: #fff; } | |
| .section { | |
| margin-top: 28px; padding-top: 20px; border-top: 1px solid #1f1f1f; | |
| } | |
| .muted { color: #666; font-size: 11px; margin-top: 4px; } | |
| #copied { | |
| display: inline-block; margin-left: 10px; color: #6bd392; | |
| opacity: 0; transition: opacity 0.3s; | |
| } | |
| #copied.show { opacity: 1; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="wrap"> | |
| <h1>Zora sandbox probe · v1</h1> | |
| <h2>What can an HTML content coin actually do on Zora?</h2> | |
| <div class="row"> | |
| <div class="label">iframe origin</div> | |
| <div class="value" id="origin">—</div> | |
| </div> | |
| <div class="row"> | |
| <div class="label">iframe location.href</div> | |
| <div class="value" id="href">—</div> | |
| </div> | |
| <div class="row"> | |
| <div class="label">is in iframe?</div> | |
| <div class="value" id="is-iframe">—</div> | |
| </div> | |
| <div class="row"> | |
| <div class="label">parent-window access</div> | |
| <div class="value" id="parent-access">—</div> | |
| </div> | |
| <div class="row"> | |
| <div class="label">top.location.href</div> | |
| <div class="value" id="top-href">—</div> | |
| </div> | |
| <div class="section"> | |
| <div class="row"> | |
| <div class="label">window.ethereum</div> | |
| <div class="value" id="window-ethereum">—</div> | |
| </div> | |
| <div class="row"> | |
| <div class="label">EIP-6963 providers</div> | |
| <div class="value" id="eip6963">—</div> | |
| </div> | |
| <div class="row"> | |
| <div class="label">eth_accounts (passive)</div> | |
| <div class="value" id="eth-accounts">—</div> | |
| </div> | |
| <div class="row"> | |
| <div class="label">eth_chainId (passive)</div> | |
| <div class="value" id="eth-chain">—</div> | |
| </div> | |
| </div> | |
| <div class="section"> | |
| <div class="row"> | |
| <div class="label">fetch public Base RPC</div> | |
| <div class="value" id="rpc-fetch">—</div> | |
| </div> | |
| <div class="row"> | |
| <div class="label">fetch IPFS gateway</div> | |
| <div class="value" id="ipfs-fetch">—</div> | |
| </div> | |
| <div class="row"> | |
| <div class="label">fetch 4626 domain</div> | |
| <div class="value" id="fourrsix-fetch">—</div> | |
| </div> | |
| <div class="row"> | |
| <div class="label">localStorage</div> | |
| <div class="value" id="localstorage">—</div> | |
| </div> | |
| <div class="row"> | |
| <div class="label">sessionStorage</div> | |
| <div class="value" id="sessionstorage">—</div> | |
| </div> | |
| <div class="row"> | |
| <div class="label">cookies</div> | |
| <div class="value" id="cookies">—</div> | |
| </div> | |
| </div> | |
| <div class="section"> | |
| <div class="muted"> | |
| Manual tests — these need your click (browsers block automatic versions): | |
| </div> | |
| <button id="test-open">Test window.open → 4626.fun</button> | |
| <button id="test-wallet">Test wallet connect (eth_requestAccounts)</button> | |
| <button id="test-sign">Test personal_sign</button> | |
| <div id="manual-result" class="muted" style="margin-top: 10px"></div> | |
| </div> | |
| <div class="section"> | |
| <div class="muted">Copy all findings into your clipboard so you can paste them back to me:</div> | |
| <button id="copy-btn">Copy results</button> | |
| <button id="refresh-btn" class="secondary">Rerun probes</button> | |
| <span id="copied">copied</span> | |
| <pre id="dump" style="margin-top: 12px; max-height: 260px; overflow: auto"></pre> | |
| </div> | |
| <div class="section muted"> | |
| Zora sandbox probe. No wallet signatures are sent without your explicit click | |
| on the manual-test buttons. Upload this .html to Zora as a content coin and | |
| view the card to run the probes. | |
| </div> | |
| </div> | |
| <script> | |
| // ─── Helpers ───────────────────────────────────────────────────────── | |
| const set = (id, html, tag) => { | |
| const el = document.getElementById(id); | |
| if (!el) return; | |
| const tagHtml = tag | |
| ? `<span class="tag ${tag.cls}">${tag.text}</span>` | |
| : ""; | |
| el.innerHTML = tagHtml + html; | |
| }; | |
| const results = {}; | |
| const store = (k, v, tag) => { results[k] = { value: v, tag: tag?.text ?? null }; }; | |
| // Tag shortcuts | |
| const pass = (text = "yes") => ({ cls: "pass", text }); | |
| const fail = (text = "no") => ({ cls: "fail", text }); | |
| const neutral = (text) => ({ cls: "neutral", text }); | |
| const pending = (text = "pending") => ({ cls: "pending", text }); | |
| // ─── 1. Basic iframe + origin info ─────────────────────────────────── | |
| (function () { | |
| try { | |
| const origin = window.location.origin || "(null origin)"; | |
| const href = window.location.href; | |
| set("origin", origin, neutral(origin.includes("null") ? "null" : "set")); | |
| store("origin", origin); | |
| set("href", href); | |
| store("href", href); | |
| } catch (e) { | |
| set("origin", String(e), fail()); | |
| set("href", String(e), fail()); | |
| } | |
| const isFrame = window.parent !== window || window.top !== window; | |
| set("is-iframe", String(isFrame), isFrame ? pass("sandboxed") : neutral("top-level")); | |
| store("is-iframe", isFrame); | |
| try { | |
| const _ = window.parent.document; // throws on cross-origin | |
| set("parent-access", "✓ parent.document readable", pass("allowed")); | |
| store("parent-access", "allowed"); | |
| } catch (e) { | |
| set("parent-access", `blocked: ${e.name}`, fail("blocked")); | |
| store("parent-access", `blocked: ${e.name}`); | |
| } | |
| try { | |
| const url = window.top.location.href; | |
| set("top-href", url || "(empty)", pass("readable")); | |
| store("top-href", url); | |
| } catch (e) { | |
| set("top-href", `blocked: ${e.name}`, fail("blocked")); | |
| store("top-href", `blocked: ${e.name}`); | |
| } | |
| })(); | |
| // ─── 2. Wallet provider detection ──────────────────────────────────── | |
| (function () { | |
| const hasEth = typeof window.ethereum !== "undefined"; | |
| if (hasEth) { | |
| const info = []; | |
| const e = window.ethereum; | |
| if (e.isMetaMask) info.push("isMetaMask"); | |
| if (e.isRabby) info.push("isRabby"); | |
| if (e.isCoinbaseWallet) info.push("isCoinbaseWallet"); | |
| if (e.isPhantom) info.push("isPhantom"); | |
| if (e.isBraveWallet) info.push("isBraveWallet"); | |
| if (Array.isArray(e.providers)) info.push(`providers[${e.providers.length}]`); | |
| set("window-ethereum", info.join(", ") || "injected (unknown flavor)", pass("injected")); | |
| store("window-ethereum", info.join(", ") || "injected"); | |
| } else { | |
| set("window-ethereum", "undefined", fail("absent")); | |
| store("window-ethereum", "undefined"); | |
| } | |
| // EIP-6963: modern wallet discovery | |
| const providers = []; | |
| const onAnnounce = (e) => { | |
| const info = e.detail?.info || {}; | |
| providers.push(`${info.name || "?"} (rdns ${info.rdns || "?"})`); | |
| renderEip6963(); | |
| }; | |
| const renderEip6963 = () => { | |
| if (providers.length === 0) { | |
| set("eip6963", "no announce events (yet)", neutral("pending")); | |
| } else { | |
| set("eip6963", providers.join(" · "), pass(`${providers.length} wallet(s)`)); | |
| store("eip6963", providers); | |
| } | |
| }; | |
| window.addEventListener("eip6963:announceProvider", onAnnounce); | |
| window.dispatchEvent(new Event("eip6963:requestProvider")); | |
| renderEip6963(); | |
| setTimeout(renderEip6963, 300); | |
| setTimeout(renderEip6963, 1500); | |
| })(); | |
| // ─── 3. Passive RPC calls (eth_accounts/chainId don't prompt user) ─── | |
| (async function () { | |
| if (typeof window.ethereum === "undefined") { | |
| set("eth-accounts", "skipped (no provider)", neutral("skip")); | |
| set("eth-chain", "skipped (no provider)", neutral("skip")); | |
| return; | |
| } | |
| try { | |
| const accounts = await window.ethereum.request({ method: "eth_accounts" }); | |
| if (Array.isArray(accounts) && accounts.length > 0) { | |
| set("eth-accounts", accounts.join(", "), pass("authorized")); | |
| store("eth-accounts", accounts); | |
| } else { | |
| set("eth-accounts", "[] (not connected to this origin)", neutral("empty")); | |
| store("eth-accounts", []); | |
| } | |
| } catch (e) { | |
| set("eth-accounts", `error: ${e.message || e}`, fail()); | |
| store("eth-accounts", `error: ${e.message || e}`); | |
| } | |
| try { | |
| const chain = await window.ethereum.request({ method: "eth_chainId" }); | |
| set("eth-chain", `${chain} (${parseInt(chain, 16)})`, pass()); | |
| store("eth-chain", chain); | |
| } catch (e) { | |
| set("eth-chain", `error: ${e.message || e}`, fail()); | |
| store("eth-chain", `error: ${e.message || e}`); | |
| } | |
| })(); | |
| // ─── 4. Network reachability ───────────────────────────────────────── | |
| set("rpc-fetch", "running…", pending()); | |
| set("ipfs-fetch", "running…", pending()); | |
| set("fourrsix-fetch", "running…", pending()); | |
| (async function () { | |
| // Base mainnet public RPC | |
| try { | |
| const r = await fetch("https://mainnet.base.org", { | |
| method: "POST", | |
| headers: { "content-type": "application/json" }, | |
| body: JSON.stringify({ jsonrpc: "2.0", method: "eth_blockNumber", params: [], id: 1 }), | |
| }); | |
| const j = await r.json(); | |
| if (j.result) { | |
| const block = parseInt(j.result, 16); | |
| set("rpc-fetch", `OK, block ${block}`, pass("allowed")); | |
| store("rpc-fetch", `block ${block}`); | |
| } else { | |
| set("rpc-fetch", `response: ${JSON.stringify(j)}`, neutral("weird")); | |
| store("rpc-fetch", JSON.stringify(j)); | |
| } | |
| } catch (e) { | |
| set("rpc-fetch", `blocked: ${e.message || e}`, fail()); | |
| store("rpc-fetch", `blocked: ${e.message || e}`); | |
| } | |
| // Generic IPFS gateway (useful if we want to load ADDITIONAL on-chain assets) | |
| try { | |
| const r = await fetch("https://ipfs.io/ipfs/QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG", { method: "HEAD" }); | |
| set("ipfs-fetch", `status ${r.status}`, r.ok ? pass("allowed") : fail()); | |
| store("ipfs-fetch", `status ${r.status}`); | |
| } catch (e) { | |
| set("ipfs-fetch", `blocked: ${e.message || e}`, fail()); | |
| store("ipfs-fetch", `blocked: ${e.message || e}`); | |
| } | |
| // 4626 domain (test cross-domain fetch with CORS) | |
| try { | |
| const r = await fetch("https://4626.fun", { method: "HEAD", mode: "cors" }); | |
| set("fourrsix-fetch", `status ${r.status} (cors mode)`, r.ok ? pass() : neutral("reachable")); | |
| store("fourrsix-fetch", `status ${r.status}`); | |
| } catch (e) { | |
| set("fourrsix-fetch", `blocked by CORS: ${e.message || e}`, fail("cors blocked")); | |
| store("fourrsix-fetch", `cors blocked: ${e.message || e}`); | |
| } | |
| })(); | |
| // ─── 5. Storage + cookies ──────────────────────────────────────────── | |
| (function () { | |
| try { | |
| localStorage.setItem("__probe", "1"); | |
| const v = localStorage.getItem("__probe"); | |
| localStorage.removeItem("__probe"); | |
| set("localstorage", `roundtrip ok (${v})`, pass("works")); | |
| store("localstorage", "works"); | |
| } catch (e) { | |
| set("localstorage", `blocked: ${e.message || e}`, fail("blocked")); | |
| store("localstorage", "blocked"); | |
| } | |
| try { | |
| sessionStorage.setItem("__probe", "1"); | |
| const v = sessionStorage.getItem("__probe"); | |
| sessionStorage.removeItem("__probe"); | |
| set("sessionstorage", `roundtrip ok (${v})`, pass("works")); | |
| store("sessionstorage", "works"); | |
| } catch (e) { | |
| set("sessionstorage", `blocked: ${e.message || e}`, fail("blocked")); | |
| store("sessionstorage", "blocked"); | |
| } | |
| try { | |
| document.cookie = "probe=1; path=/"; | |
| const visible = document.cookie.includes("probe=1"); | |
| set("cookies", visible ? "readable/writable" : "read-only", visible ? pass() : neutral("read-only")); | |
| store("cookies", visible ? "rw" : "read-only"); | |
| } catch (e) { | |
| set("cookies", `blocked: ${e.message || e}`, fail("blocked")); | |
| store("cookies", "blocked"); | |
| } | |
| })(); | |
| // ─── 6. Manual tests (user-click-gated) ────────────────────────────── | |
| const manualEl = document.getElementById("manual-result"); | |
| document.getElementById("test-open").addEventListener("click", () => { | |
| try { | |
| const w = window.open("https://4626.fun?from=zora-probe", "_blank"); | |
| if (w) { | |
| manualEl.textContent = "window.open returned a handle — popup ALLOWED ✓"; | |
| results["window.open"] = { value: "allowed", tag: "allowed" }; | |
| } else { | |
| manualEl.textContent = "window.open returned null — BLOCKED by sandbox or popup blocker"; | |
| results["window.open"] = { value: "blocked (null)", tag: "blocked" }; | |
| } | |
| } catch (e) { | |
| manualEl.textContent = `window.open threw: ${e.message || e}`; | |
| results["window.open"] = { value: `threw: ${e.message}`, tag: "error" }; | |
| } | |
| }); | |
| document.getElementById("test-wallet").addEventListener("click", async () => { | |
| if (typeof window.ethereum === "undefined") { | |
| manualEl.textContent = "No window.ethereum — nothing to call"; | |
| return; | |
| } | |
| try { | |
| manualEl.textContent = "Requesting accounts… approve in wallet popup"; | |
| const accounts = await window.ethereum.request({ method: "eth_requestAccounts" }); | |
| manualEl.textContent = `accounts: ${JSON.stringify(accounts)} ✓ WALLET INTERACTION WORKS IN SANDBOX`; | |
| results["eth_requestAccounts"] = { value: accounts, tag: "allowed" }; | |
| } catch (e) { | |
| manualEl.textContent = `wallet rejected or blocked: ${e.message || e}`; | |
| results["eth_requestAccounts"] = { value: `error: ${e.message}`, tag: "error" }; | |
| } | |
| }); | |
| document.getElementById("test-sign").addEventListener("click", async () => { | |
| if (typeof window.ethereum === "undefined") { | |
| manualEl.textContent = "No window.ethereum — nothing to call"; | |
| return; | |
| } | |
| try { | |
| const accs = await window.ethereum.request({ method: "eth_accounts" }); | |
| if (!accs || !accs.length) { | |
| manualEl.textContent = "No authorized account. Click 'Test wallet connect' first."; | |
| return; | |
| } | |
| manualEl.textContent = "Requesting signature… approve in wallet popup"; | |
| const msg = `Zora sandbox probe · signing from ${window.location.origin}`; | |
| const sig = await window.ethereum.request({ | |
| method: "personal_sign", | |
| params: [msg, accs[0]], | |
| }); | |
| manualEl.textContent = `sig: ${sig.slice(0, 20)}… ✓ SIGNING WORKS IN SANDBOX`; | |
| results["personal_sign"] = { value: sig, tag: "allowed" }; | |
| } catch (e) { | |
| manualEl.textContent = `signature rejected or blocked: ${e.message || e}`; | |
| results["personal_sign"] = { value: `error: ${e.message}`, tag: "error" }; | |
| } | |
| }); | |
| // ─── 7. Copy dump ──────────────────────────────────────────────────── | |
| const renderDump = () => { | |
| const payload = { | |
| userAgent: navigator.userAgent, | |
| ...Object.fromEntries(Object.entries(results).map(([k, v]) => [k, v.value])), | |
| }; | |
| document.getElementById("dump").textContent = JSON.stringify(payload, null, 2); | |
| }; | |
| setInterval(renderDump, 500); | |
| renderDump(); | |
| document.getElementById("copy-btn").addEventListener("click", async () => { | |
| const text = document.getElementById("dump").textContent; | |
| try { | |
| await navigator.clipboard.writeText(text); | |
| } catch { | |
| // fallback | |
| const ta = document.createElement("textarea"); | |
| ta.value = text; document.body.appendChild(ta); ta.select(); | |
| document.execCommand("copy"); document.body.removeChild(ta); | |
| } | |
| const toast = document.getElementById("copied"); | |
| toast.classList.add("show"); | |
| setTimeout(() => toast.classList.remove("show"), 1200); | |
| }); | |
| document.getElementById("refresh-btn").addEventListener("click", () => { | |
| window.location.reload(); | |
| }); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment