Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions playground/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ <h1>Arkade Playground</h1>
<span class="version" id="compiler-version"></span>
</div>
<div class="header-right">
<div class="network-selector">
<select id="network-select">
<option value="bitcoin">Bitcoin</option>
<option value="mutinynet">Mutinynet</option>
<option value="signet">Signet</option>
</select>
Comment on lines +18 to +22

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

<select> is missing an accessible label.

Screen readers will announce this as an unlabeled combobox.

♿ Proposed fix
-                <select id="network-select">
+                <select id="network-select" aria-label="Network">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<select id="network-select">
<option value="bitcoin">Bitcoin</option>
<option value="mutinynet">Mutinynet</option>
<option value="signet">Signet</option>
</select>
<select id="network-select" aria-label="Network">
<option value="bitcoin">Bitcoin</option>
<option value="mutinynet">Mutinynet</option>
<option value="signet">Signet</option>
</select>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@playground/index.html` around lines 18 - 22, The <select> element with id
"network-select" is missing an accessible label; add an associated visible
<label> (e.g., <label for="network-select">Network</label>) placed before the
select, or if a visible label is not desired, add an explicit aria-label or
aria-labelledby that references an existing element, ensuring screen readers
announce the control (update the element with id "network-select" accordingly).

<span class="network-status" id="network-status" title="Fetching..."></span>
</div>
<button id="compile-btn" class="btn-primary">
<i class="fas fa-play"></i> Compile
</button>
Expand Down
80 changes: 75 additions & 5 deletions playground/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`;
}
}
}
Comment on lines +16 to +43

@coderabbitai coderabbitai Bot Feb 23, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Stale-response race condition + no fetch timeout in fetchNetworkInfo.

Race condition: networkInfo and the status indicator are written unconditionally after await. When the startup fetch (line 1421, fire-and-forget) is still in-flight and the user switches networks, whichever request completes last overwrites networkInfo for the currently selected network. A slow bitcoin response completing after a mutinynet switch will silently inject bitcoin's signerPubkey into new files created on mutinynet.

No timeout: a slow or unreachable host leaves the status indicator in "loading" until the browser's own timeout (~30 s+), making the error state very slow to appear.

🐛 Proposed fix
 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`);
+        const controller = new AbortController();
+        const timeoutId = setTimeout(() => controller.abort(), 5000);
+        const resp = await fetch(`${net.url}/v1/info`, { signal: controller.signal });
+        clearTimeout(timeoutId);
         if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
         const data = await resp.json();
+        if (networkKey !== selectedNetwork) return; // discard stale response
         networkInfo = data;
         if (statusEl) {
             statusEl.className = 'network-status connected';
             statusEl.title = `${net.name}: connected`;
         }
     } catch (e) {
+        if (networkKey !== selectedNetwork) return; // discard stale error
         console.warn(`Failed to fetch ${net.name} info:`, e);
         networkInfo = null;
         if (statusEl) {
             statusEl.className = 'network-status error';
             statusEl.title = `${net.name}: unreachable`;
         }
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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`;
}
}
}
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 controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
const resp = await fetch(`${net.url}/v1/info`, { signal: controller.signal });
clearTimeout(timeoutId);
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const data = await resp.json();
if (networkKey !== selectedNetwork) return; // discard stale response
networkInfo = data;
if (statusEl) {
statusEl.className = 'network-status connected';
statusEl.title = `${net.name}: connected`;
}
} catch (e) {
if (networkKey !== selectedNetwork) return; // discard stale error
console.warn(`Failed to fetch ${net.name} info:`, e);
networkInfo = null;
if (statusEl) {
statusEl.className = 'network-status error';
statusEl.title = `${net.name}: unreachable`;
}
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@playground/main.js` around lines 16 - 43, fetchNetworkInfo has a
stale-response race and no timeout: make each call use an AbortController with a
short timeout (clear the timer on completion) and attach a per-call token (e.g.,
incrementing requestId or store the current networkKey snapshot) so you only
assign networkInfo and update statusEl when the token matches the latest
outstanding request for that network; specifically, in fetchNetworkInfo use
NETWORKS[networkKey], create an AbortController + setTimeout to abort after the
desired timeout, await fetch(signal), and only set networkInfo and change
statusEl.className/title if the saved requestId/current selected networkKey
still matches, and ensure you handle abort/errors by setting networkInfo=null
and status to error, clearing timers and not leaking controllers.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can confirm this @tiero. I switched to signet from mutinynet and then back to mutinynet, then got a red dot. Switching to bitcoin and back again to mutinynet I got a green dot back.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!


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';
Comment on lines +53 to +56

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

networkInfo.signerPubkey is not guarded against undefined.

If the /v1/info response omits the field (or the schema changes), the generated template will contain the literal string "undefined".

🐛 Proposed fix
 if (networkInfo) {
-    serverComment = `  // Network: ${net.name} (${net.url})\n  // Signer pubkey: ${networkInfo.signerPubkey}\n`;
+    const pubkeyLine = networkInfo.signerPubkey
+        ? `  // Signer pubkey: ${networkInfo.signerPubkey}\n`
+        : '';
+    serverComment = `  // Network: ${net.name} (${net.url})\n${pubkeyLine}`;
     exitValue = networkInfo.unilateralExitDelay || '144';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (networkInfo) {
serverComment = ` // Network: ${net.name} (${net.url})\n // Signer pubkey: ${networkInfo.signerPubkey}\n`;
exitValue = networkInfo.unilateralExitDelay || '144';
renewValue = '4032';
if (networkInfo) {
const pubkeyLine = networkInfo.signerPubkey
? ` // Signer pubkey: ${networkInfo.signerPubkey}\n`
: '';
serverComment = ` // Network: ${net.name} (${net.url})\n${pubkeyLine}`;
exitValue = networkInfo.unilateralExitDelay || '144';
renewValue = '4032';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@playground/main.js` around lines 53 - 56, The template generation uses
networkInfo.signerPubkey directly when building serverComment, which will
interpolate "undefined" if absent; update the logic in playground/main.js (where
serverComment is set using networkInfo and net) to guard the field — e.g., check
networkInfo && networkInfo.signerPubkey (or use a safe fallback like an empty
string or "unknown") before interpolating into serverComment so the generated
template never contains the literal "undefined".

}

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: {
Expand Down Expand Up @@ -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)) {
Expand All @@ -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);
}
Expand Down Expand Up @@ -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);
}
Expand All @@ -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);
Expand Down Expand Up @@ -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();

Expand Down
48 changes: 48 additions & 0 deletions playground/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down