From 51dff22ec765d80e89b27c6e9f96e8b55707eb6b Mon Sep 17 00:00:00 2001 From: Marco Argentieri <3596602+tiero@users.noreply.github.com> Date: Mon, 23 Feb 2026 13:56:57 +0100 Subject: [PATCH] feat: add network selector to playground with auto-prefill from Arkade API Adds a dropdown in the header bar to switch between Bitcoin, Mutinynet, and Signet networks. On selection, fetches /v1/info from the server and uses the signerPubkey and unilateralExitDelay to prefill the options block in newly created files. Existing files are never modified. Selection persists across sessions via localStorage. Co-Authored-By: Claude Opus 4.6 --- playground/index.html | 8 +++++ playground/main.js | 80 ++++++++++++++++++++++++++++++++++++++++--- playground/style.css | 48 ++++++++++++++++++++++++++ 3 files changed, 131 insertions(+), 5 deletions(-) diff --git a/playground/index.html b/playground/index.html index dade527..12c5ce5 100644 --- a/playground/index.html +++ b/playground/index.html @@ -14,6 +14,14 @@

Arkade Playground

+
+ + +
diff --git a/playground/main.js b/playground/main.js index 93a4af3..55aede5 100644 --- a/playground/main.js +++ b/playground/main.js @@ -3,6 +3,62 @@ import initWasm, { compile, version, validate, init as initPanicHook } from './pkg/arkade_compiler.js'; import * as contracts from './contracts.js'; +// ── Network configuration ──────────────────────────────────────── +const NETWORKS = { + bitcoin: { name: 'Bitcoin', url: 'https://arkade.computer' }, + mutinynet: { name: 'Mutinynet', url: 'https://mutinynet.arkade.sh' }, + signet: { name: 'Signet', url: 'https://signet.arkade.sh' }, +}; + +let selectedNetwork = 'bitcoin'; +let networkInfo = null; // cached API response for current network + +async function fetchNetworkInfo(networkKey) { + const net = NETWORKS[networkKey]; + if (!net) return; + + const statusEl = document.getElementById('network-status'); + if (statusEl) { + statusEl.className = 'network-status loading'; + statusEl.title = 'Fetching...'; + } + + try { + const resp = await fetch(`${net.url}/v1/info`); + if (!resp.ok) throw new Error(`HTTP ${resp.status}`); + const data = await resp.json(); + networkInfo = data; + if (statusEl) { + statusEl.className = 'network-status connected'; + statusEl.title = `${net.name}: connected`; + } + } catch (e) { + console.warn(`Failed to fetch ${net.name} info:`, e); + networkInfo = null; + if (statusEl) { + statusEl.className = 'network-status error'; + statusEl.title = `${net.name}: unreachable`; + } + } +} + +function getDefaultTemplate(fileName) { + const displayName = fileName.replace(/\.ark$/, ''); + const net = NETWORKS[selectedNetwork]; + + let serverComment = ''; + let exitValue = '144'; + let renewValue = '1008'; + + if (networkInfo) { + serverComment = ` // Network: ${net.name} (${net.url})\n // Signer pubkey: ${networkInfo.signerPubkey}\n`; + exitValue = networkInfo.unilateralExitDelay || '144'; + renewValue = '4032'; + } + + return `// ${displayName} Contract\n\noptions {\n${serverComment} server = server;\n\n // Batch expiry: ~4 weeks\n renew = ${renewValue};\n\n // Unilateral exit delay\n exit = ${exitValue};\n}\n\ncontract ${displayName}(\n pubkey user\n) {\n function spend(signature userSig) {\n require(checkSig(userSig, user));\n }\n}\n`; +} + // Projects: collections of related contracts const projects = { stability: { @@ -41,7 +97,8 @@ const STORAGE_KEY = 'arkade-playground'; function saveToStorage() { const data = { projects: {}, - examples: {} + examples: {}, + selectedNetwork }; // Only save user-created / modified entries for (const [id, proj] of Object.entries(projects)) { @@ -68,6 +125,9 @@ function loadFromStorage() { examples[id] = ex; } } + if (data.selectedNetwork && NETWORKS[data.selectedNetwork]) { + selectedNetwork = data.selectedNetwork; + } } catch (e) { console.warn('Failed to load from localStorage:', e); } @@ -158,8 +218,7 @@ function createFileInFolder(folderId, fileName) { const project = projects[folderId]; if (!project) return; if (project.files[fileName]) return; // already exists - const defaultCode = `// ${fileName}\n\noptions {\n server = serverPk;\n exit = 144;\n}\n\ncontract MyContract(\n pubkey user\n) {\n function spend(signature userSig) {\n require(checkSig(userSig, user));\n }\n}\n`; - project.files[fileName] = defaultCode; + project.files[fileName] = getDefaultTemplate(fileName); saveToStorage(); selectProjectFile(folderId, fileName); } @@ -168,8 +227,7 @@ function createStandaloneFile(name) { if (!name.endsWith('.ark')) name += '.ark'; const displayName = name.replace(/\.ark$/, ''); const id = uniqueId(generateId(displayName), examples); - const defaultCode = `// ${displayName} Contract\n\noptions {\n server = serverPk;\n exit = 144;\n}\n\ncontract ${displayName}(\n pubkey user\n) {\n function spend(signature userSig) {\n require(checkSig(userSig, user));\n }\n}\n`; - examples[id] = { name: displayName, code: defaultCode }; + examples[id] = { name: displayName, code: getDefaultTemplate(name) }; expandedFolders.add('_examples'); saveToStorage(); selectExample(id); @@ -1350,6 +1408,18 @@ document.addEventListener('DOMContentLoaded', () => { // Load user data from localStorage loadFromStorage(); + // Set up network selector + const networkSelect = document.getElementById('network-select'); + networkSelect.value = selectedNetwork; + networkSelect.addEventListener('change', async () => { + selectedNetwork = networkSelect.value; + saveToStorage(); + await fetchNetworkInfo(selectedNetwork); + }); + + // Fetch network info on startup + fetchNetworkInfo(selectedNetwork); + // Begin decoding URL hash early (async) so it's ready when Monaco is up window._urlCodePromise = loadFromUrl(); diff --git a/playground/style.css b/playground/style.css index e784fb0..e988a65 100644 --- a/playground/style.css +++ b/playground/style.css @@ -82,6 +82,54 @@ select:hover { border-color: var(--accent); } +/* Network Selector */ +.network-selector { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.network-selector select { + background: var(--bg-tertiary); + color: var(--text-primary); + border: 1px solid var(--border); + padding: 0.4rem 0.6rem; + border-radius: 4px; + font-size: 0.8125rem; + cursor: pointer; +} + +.network-selector select:hover { + border-color: var(--accent); +} + +.network-status { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--text-muted); + flex-shrink: 0; + transition: background 0.3s; +} + +.network-status.loading { + background: var(--warning); + animation: status-pulse 1s ease infinite; +} + +.network-status.connected { + background: var(--success); +} + +.network-status.error { + background: var(--error); +} + +@keyframes status-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.4; } +} + .btn-primary { background: var(--accent); color: white;