<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>svmscope</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&family=Space+Grotesk:wght@500;600;700&family=Instrument+Serif:ital@0;1&display=swap" rel="stylesheet" />
<style>
/* ============================================================
svmscope — design system
Tokens → base → layout → components. 4px spacing scale,
modular type scale, one accent color, semantic green/red.
============================================================ */
:root {
/* surfaces */
--bg-0: #0b0d12; /* page */
--bg-1: #10131a; /* raised card */
--bg-2: #161a24; /* higher / hover */
--bg-inset: #08090d; /* wells: logs, inputs */
/* hairlines */
--border: rgba(255,255,255,.08);
--border-strong: rgba(255,255,255,.16);
/* text */
--text-1: #f2f4f9; /* primary */
--text-2: #a9b2c3; /* secondary */
--text-3: #69738a; /* tertiary / labels */
/* brand & semantics */
--brand: #9945ff;
--brand-strong: #a964ff;
--brand-soft: rgba(153,69,255,.12);
--brand-border: rgba(153,69,255,.35);
--green: #34d399;
--green-soft: rgba(52,211,153,.12);
--red: #f87171;
--red-soft: rgba(248,113,113,.12);
--amber: #fbbf24;
--amber-soft: rgba(251,191,36,.12);
--cyan: #22d3ee;
/* type */
--font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
--font-display: "Space Grotesk", "Inter", sans-serif;
--font-serif: "Instrument Serif", Georgia, serif;
/* radii */
--r-sm: 8px;
--r-md: 10px;
--r-lg: 14px;
--r-full: 999px;
/* spacing (4px scale) */
--s-1: 4px; --s-2: 8px; --s-3: 12px; --s-4: 16px;
--s-5: 20px; --s-6: 24px; --s-8: 32px; --s-10: 40px; --s-12: 48px;
/* elevation */
--shadow-1: 0 1px 2px rgba(0,0,0,.4);
--shadow-2: 0 4px 16px -4px rgba(0,0,0,.5);
--shadow-3: 0 12px 40px -12px rgba(0,0,0,.6);
--ring: 0 0 0 3px rgba(153,69,255,.22);
/* motion */
--ease: cubic-bezier(.2, .8, .2, 1);
--t-fast: 140ms;
--t-med: 220ms;
}
/* ---------- base ---------- */
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
font-family: var(--font-sans);
font-size: 14px;
line-height: 1.55;
letter-spacing: -0.006em;
color: var(--text-1);
background: var(--bg-0);
min-height: 100vh;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
font-feature-settings: "cv11", "ss01";
}
/* quiet two-tone aurora — brand left, cyan right */
body::before {
content: "";
position: fixed; inset: 0 0 auto 0; height: 70vh;
z-index: -1; pointer-events: none;
background:
radial-gradient(42% 60% at 20% -12%, rgba(153,69,255,.14), transparent 70%),
radial-gradient(36% 52% at 80% -14%, rgba(34,211,238,.07), transparent 70%),
radial-gradient(60% 100% at 50% -30%, rgba(153,69,255,.07), transparent 70%);
}
::selection { background: rgba(153,69,255,.30); }
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-thumb {
background: var(--border-strong); border-radius: var(--r-full);
border: 3px solid transparent; background-clip: padding-box;
}
::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,.24); background-clip: padding-box; border: 3px solid transparent; }
@keyframes fade-up { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
@keyframes spin { to { transform: rotate(360deg); } }
@keyframes pulse-dot {
0% { box-shadow: 0 0 0 0 rgba(52,211,153,.45); }
70%, 100% { box-shadow: 0 0 0 7px rgba(52,211,153,0); }
}
/* ---------- top bar ---------- */
.topbar {
position: sticky; top: 0; z-index: 40;
background: rgba(11,13,18,.78);
backdrop-filter: blur(16px) saturate(150%);
-webkit-backdrop-filter: blur(16px) saturate(150%);
border-bottom: 1px solid var(--border);
}
.topbar-inner {
max-width: 1080px; margin: 0 auto;
padding: var(--s-3) var(--s-6);
display: flex; align-items: center; gap: var(--s-5);
}
.logo { display: flex; align-items: center; gap: var(--s-3); flex: 0 0 auto; }
.logo .mark {
width: 30px; height: 30px; border-radius: var(--r-sm);
display: grid; place-items: center;
font-size: 15px; font-weight: 700; color: #fff;
background: linear-gradient(135deg, #a964ff, #7b2ff2);
box-shadow: inset 0 1px 0 rgba(255,255,255,.28), 0 2px 8px rgba(123,47,242,.45);
}
.logo b { font-family: var(--font-display); font-size: 16px; font-weight: 600; letter-spacing: -0.01em; }
.logo .tag { color: var(--text-3); font-size: 13px; }
.logo { cursor: pointer; }
.nav { display: flex; gap: var(--s-1); flex: 0 0 auto; background: var(--bg-2);
border: 1px solid var(--border); border-radius: var(--r-md); padding: 3px; }
.navtab {
appearance: none; border: 0; cursor: pointer; background: transparent;
color: var(--text-3); padding: 6px var(--s-3); border-radius: 7px;
font-family: var(--font-sans); font-size: 13px; font-weight: 550; white-space: nowrap;
transition: color var(--t-fast), background var(--t-fast);
}
.navtab:hover { color: var(--text-1); }
.navtab.on { background: var(--bg-1); color: var(--text-1); box-shadow: var(--shadow-1); }
@media (max-width: 860px) { .nav { display: none; } }
.search { display: flex; gap: var(--s-2); flex: 1; min-width: 0; }
.search input {
flex: 1; min-width: 0;
background: var(--bg-inset);
border: 1px solid var(--border);
color: var(--text-1);
padding: 9px var(--s-4);
border-radius: var(--r-md);
font-family: var(--font-mono); font-size: 12.5px;
transition: border-color var(--t-fast), box-shadow var(--t-fast);
}
.search input::placeholder { color: var(--text-3); }
.search input:hover { border-color: var(--border-strong); }
.search input:focus { outline: none; border-color: var(--brand-border); box-shadow: var(--ring); }
.search .cluster {
flex: 0 0 auto;
background: var(--bg-inset);
border: 1px solid var(--border);
color: var(--text-2);
padding: 9px var(--s-3);
border-radius: var(--r-md);
font-family: var(--font-sans); font-size: 13px; font-weight: 500;
cursor: pointer;
transition: border-color var(--t-fast), color var(--t-fast);
}
.search .cluster:hover { border-color: var(--border-strong); color: var(--text-1); }
.search .cluster:focus { outline: none; border-color: var(--brand-border); box-shadow: var(--ring); }
.search .customrpc {
flex: 0 1 220px; min-width: 0;
font-family: var(--font-mono); font-size: 12px; color: var(--text-1);
}
.search .customrpc::placeholder { color: var(--text-3); }
@media (max-width: 720px) { .search .customrpc { display: none; } }
/* ---------- buttons ---------- */
.btn {
appearance: none; border: 0; cursor: pointer;
display: inline-flex; align-items: center; justify-content: center; gap: var(--s-2);
padding: 9px var(--s-5);
border-radius: var(--r-md);
font-family: var(--font-sans); font-size: 13.5px; font-weight: 600; letter-spacing: -0.006em;
color: #fff;
background: linear-gradient(180deg, #ab68ff, #7f24f5);
box-shadow: inset 0 1px 0 rgba(255,255,255,.22), var(--shadow-1), 0 0 20px -8px rgba(153,69,255,.55);
transition: background var(--t-fast), transform var(--t-fast) var(--ease), box-shadow var(--t-fast), filter var(--t-fast);
flex: 0 0 auto;
}
.btn:hover { filter: brightness(1.12); transform: translateY(-1px); box-shadow: inset 0 1px 0 rgba(255,255,255,.22), var(--shadow-2), 0 0 26px -8px rgba(153,69,255,.7); }
.btn:active { transform: none; }
.btn:focus-visible { outline: none; box-shadow: var(--ring); }
.btn:disabled { opacity: .45; cursor: default; transform: none; box-shadow: none; }
.btn.ghost {
background: transparent; color: var(--text-2);
border: 1px solid var(--border); box-shadow: none; font-weight: 550;
}
.btn.ghost:hover { color: var(--text-1); border-color: var(--border-strong); background: var(--bg-2); transform: none; box-shadow: none; filter: none; }
.btn.sm { padding: 6px var(--s-3); font-size: 12.5px; border-radius: var(--r-sm); }
/* ---------- layout ---------- */
main { max-width: 1080px; margin: 0 auto; padding: var(--s-6) var(--s-6) 96px; }
/* ---------- hero (landing) ---------- */
.hero {
display: flex; flex-direction: column; align-items: center;
text-align: center;
padding: clamp(40px, 9vh, 96px) 0 var(--s-10);
animation: fade-up .5s var(--ease) both;
}
.hero .eyebrow {
display: inline-flex; align-items: center; gap: var(--s-3);
margin-bottom: var(--s-6);
font-family: var(--font-mono); font-size: 11px; font-weight: 500;
text-transform: uppercase; letter-spacing: .22em;
color: var(--text-3);
}
.hero .eyebrow .bkt { color: var(--brand-strong); }
.hero .eyebrow .pulse {
width: 6px; height: 6px; border-radius: var(--r-full);
background: var(--green);
animation: pulse-dot 2.6s ease-out infinite;
}
.hero h1 {
margin: 0 0 var(--s-5);
font-family: var(--font-display);
font-size: clamp(46px, 7.6vw, 96px);
font-weight: 700;
text-transform: uppercase;
letter-spacing: -0.025em; line-height: 0.99;
max-width: 980px;
}
/* cinematic line-mask reveal */
.hero h1 .h1-line { display: block; overflow: hidden; padding-bottom: .08em; margin-bottom: -.08em; }
.hero h1 .h1-inner {
display: block;
transform: translateY(118%);
animation: rise 1s cubic-bezier(.19, 1, .22, 1) forwards;
}
.hero h1 .h1-line:nth-child(1) .h1-inner { animation-delay: .05s; }
.hero h1 .h1-line:nth-child(2) .h1-inner { animation-delay: .16s; }
.hero h1 .h1-line:nth-child(3) .h1-inner { animation-delay: .27s; }
@keyframes rise { to { transform: translateY(0); } }
.hero h1 .grad {
background: linear-gradient(95deg, var(--brand-strong), var(--cyan), #ec4899, var(--brand-strong));
background-size: 280% 100%;
-webkit-background-clip: text; background-clip: text; color: transparent;
animation: shimmer 7s linear infinite;
}
@keyframes shimmer { to { background-position: 280% 0; } }
/* the italic-serif counterpoint word — lowercase against the caps */
.hero h1 .serif {
font-family: var(--font-serif); font-style: italic; font-weight: 400;
text-transform: lowercase;
letter-spacing: -0.01em;
padding-right: 0.06em; /* italic overhang */
}
/* hollow outlined word, filled on hover */
.hero h1 .hollow {
color: transparent;
-webkit-text-stroke: 1.5px rgba(242,244,249,.85);
transition: color var(--t-med) var(--ease);
}
.hero h1 .hollow:hover { color: var(--text-1); }
.hero p {
margin: 0; max-width: 560px;
color: var(--text-2);
font-size: 15.5px; line-height: 1.65;
animation: fade-up .6s var(--ease) .22s both;
}
.hero p .serif { font-family: var(--font-serif); font-style: italic; font-size: 1.12em; color: var(--text-1); }
/* film grain over everything, barely-there */
body::after {
content: ""; position: fixed; inset: -50%;
z-index: 9999; pointer-events: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='240' height='240' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E");
opacity: .035;
animation: grain 9s steps(10) infinite;
}
@keyframes grain {
0%,100% { transform: translate(0,0); } 10% { transform: translate(-3%,-6%); }
20% { transform: translate(-8%,3%); } 30% { transform: translate(4%,-8%); }
40% { transform: translate(-3%,8%); } 50% { transform: translate(-8%,4%); }
60% { transform: translate(8%,0); } 70% { transform: translate(0,6%); }
80% { transform: translate(3%,10%); } 90% { transform: translate(-6%,3%); }
}
/* drifting aurora blobs — the page breathes */
.aurora { position: fixed; inset: 0; z-index: -2; overflow: hidden; pointer-events: none; }
.aurora b {
position: absolute; display: block; border-radius: 50%;
filter: blur(90px); will-change: transform;
}
.aurora .a1 {
width: 640px; height: 640px; left: -12%; top: -22%;
background: radial-gradient(circle, rgba(153,69,255,.20), transparent 62%);
animation: drift1 26s ease-in-out infinite alternate;
}
.aurora .a2 {
width: 560px; height: 560px; right: -10%; top: -14%;
background: radial-gradient(circle, rgba(34,211,238,.12), transparent 62%);
animation: drift2 32s ease-in-out infinite alternate;
}
.aurora .a3 {
width: 480px; height: 480px; left: 38%; top: 30%;
background: radial-gradient(circle, rgba(236,72,153,.08), transparent 62%);
animation: drift3 38s ease-in-out infinite alternate;
}
@keyframes drift1 { to { transform: translate(14vw, 12vh) scale(1.15); } }
@keyframes drift2 { to { transform: translate(-12vw, 16vh) scale(0.9); } }
@keyframes drift3 { to { transform: translate(-10vw, -14vh) scale(1.2); } }
/* cursor spotlight — a soft beam that follows the pointer */
#spot {
position: fixed; left: 0; top: 0; z-index: -1;
width: 860px; height: 860px; border-radius: 50%;
background: radial-gradient(circle, rgba(153,69,255,.085), rgba(34,211,238,.03) 45%, transparent 65%);
pointer-events: none;
will-change: transform;
}
/* the self-typing replay terminal */
.term-shell { width: min(780px, 100%); margin-top: var(--s-10); perspective: 1100px; }
.term {
text-align: left;
border-radius: var(--r-lg);
border: 1px solid transparent;
background:
linear-gradient(rgba(10,11,16,.92), rgba(10,11,16,.92)) padding-box,
linear-gradient(160deg, rgba(153,69,255,.45), rgba(255,255,255,.08) 38%, rgba(34,211,238,.35)) border-box;
box-shadow: var(--shadow-3), 0 0 80px -30px rgba(153,69,255,.55);
overflow: hidden;
transition: transform .25s var(--ease);
will-change: transform;
}
.term-bar {
display: flex; align-items: center; gap: var(--s-2);
padding: 10px var(--s-4);
border-bottom: 1px solid var(--border);
background: rgba(255,255,255,.025);
}
.term-bar .td { width: 11px; height: 11px; border-radius: var(--r-full); }
.term-bar .td:nth-child(1) { background: #ff5f57; }
.term-bar .td:nth-child(2) { background: #febc2e; }
.term-bar .td:nth-child(3) { background: #28c840; }
.term-bar .tt {
flex: 1; text-align: center;
font-family: var(--font-mono); font-size: 11px; color: var(--text-3);
}
.term-body {
padding: var(--s-4) var(--s-5) var(--s-5);
font-family: var(--font-mono); font-size: 12.5px; line-height: 1.9;
min-height: 244px;
color: var(--text-2);
}
.term-body .t-line { white-space: pre-wrap; word-break: break-all; }
.term-body .t-p { color: var(--green); font-weight: 600; }
.term-body .t-ok { color: var(--green); font-weight: 600; }
.term-body .t-bad { color: var(--red); font-weight: 600; }
.term-body .t-br { color: var(--brand-strong); font-weight: 600; }
.term-body .t-cy { color: var(--cyan); }
.term-body .t-dim { color: var(--text-3); }
.term-body b { color: var(--text-1); font-weight: 600; }
.caret {
display: inline-block; width: 7px; height: 15px; vertical-align: -2px;
background: var(--brand-strong);
animation: blink 1.05s steps(1) infinite;
}
@keyframes blink { 50% { opacity: 0; } }
/* scroll-in reveals */
.reveal { opacity: 0; transform: translateY(26px); transition: opacity .8s var(--ease), transform .8s var(--ease); }
.reveal.in { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) {
.aurora b, .hero h1 .grad, .caret, .ticker-track, body::after { animation: none !important; }
.hero h1 .h1-inner { animation-duration: 0.01s; }
#spot { display: none; }
.reveal { opacity: 1; transform: none; transition: none; }
}
/* full-bleed replay ticker — real output, endlessly scrolling */
.ticker {
width: 100vw; margin: var(--s-10) calc(50% - 50vw) 0;
border-top: 1px solid var(--border); border-bottom: 1px solid var(--border);
background: rgba(8,9,13,.6);
overflow: hidden; white-space: nowrap;
-webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
}
.ticker:hover .ticker-track { animation-play-state: paused; }
.ticker-track {
display: inline-flex; align-items: center;
padding: 11px 0;
animation: ticker 46s linear infinite;
will-change: transform;
}
@keyframes ticker { to { transform: translateX(-50%); } }
.ticker .ti {
display: inline-flex; align-items: center; gap: var(--s-2);
font-family: var(--font-mono); font-size: 11.5px; letter-spacing: .02em;
color: var(--text-3);
padding: 0 var(--s-5);
}
.ticker .ti b { color: var(--text-2); font-weight: 500; }
.ticker .ti .ok { color: var(--green); }
.ticker .ti .bad { color: var(--red); }
.ticker .ti .br { color: var(--brand-strong); }
.ticker .ti .cy { color: var(--cyan); }
.ticker .sep { color: var(--border-strong); font-size: 9px; }
.hero .hint { margin-top: var(--s-8); color: var(--text-3); font-size: 13px; }
.hero .hint kbd {
font-family: var(--font-mono); font-size: 11.5px; color: var(--text-2);
background: var(--bg-1); border: 1px solid var(--border); border-bottom-width: 2px;
border-radius: 6px; padding: 2px 7px; margin: 0 2px;
}
/* faint blueprint grid behind the headline, fading out radially */
.hero { position: relative; }
.hero::before {
content: ""; position: absolute; inset: -24px -80px auto; height: 560px;
z-index: -1; pointer-events: none;
background-image:
linear-gradient(rgba(255,255,255,.04) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,.04) 1px, transparent 1px);
background-size: 44px 44px;
-webkit-mask-image: radial-gradient(58% 62% at 50% 34%, #000 0%, transparent 74%);
mask-image: radial-gradient(58% 62% at 50% 34%, #000 0%, transparent 74%);
}
/* the hero's own search — the main way in */
.hero-search {
margin-top: var(--s-8);
width: min(720px, 100%);
display: flex; gap: var(--s-2); align-items: center;
padding: 7px 7px 7px var(--s-4);
border-radius: 16px;
border: 1px solid transparent;
background:
linear-gradient(var(--bg-1), var(--bg-1)) padding-box,
linear-gradient(120deg, rgba(153,69,255,.55), rgba(34,211,238,.30) 50%, rgba(153,69,255,.55)) border-box;
box-shadow: var(--shadow-3), 0 0 54px -20px rgba(153,69,255,.5);
transition: box-shadow var(--t-med) var(--ease);
}
.hero-search:focus-within { box-shadow: var(--shadow-3), 0 0 64px -16px rgba(153,69,255,.65); }
.hero-search .hs-icon { color: var(--text-3); flex: 0 0 auto; display: grid; place-items: center; }
.hero-search input {
flex: 1; min-width: 0;
background: transparent; border: 0;
color: var(--text-1);
font-family: var(--font-mono); font-size: 13px;
padding: 10px 4px;
}
.hero-search input::placeholder { color: var(--text-3); }
.hero-search input:focus { outline: none; }
/* one-click live examples */
.examples {
margin-top: var(--s-5);
display: flex; gap: var(--s-2); flex-wrap: wrap; justify-content: center; align-items: center;
}
.examples .lbl { color: var(--text-3); font-size: 12.5px; margin-right: 2px; }
.ex-chip {
appearance: none; cursor: pointer;
display: inline-flex; align-items: center; gap: 7px;
background: var(--bg-1); border: 1px solid var(--border);
color: var(--text-2);
border-radius: var(--r-full); padding: 6px 13px;
font-family: var(--font-sans); font-size: 12.5px; font-weight: 550;
transition: color var(--t-fast), border-color var(--t-fast), background var(--t-fast), transform var(--t-fast) var(--ease);
}
.ex-chip:hover { color: var(--text-1); border-color: var(--brand-border); background: var(--brand-soft); transform: translateY(-1px); }
.ex-chip .dot { width: 6px; height: 6px; border-radius: var(--r-full); flex: 0 0 auto; }
/* what-it-does grid */
.features {
display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--s-4);
width: 100%; max-width: 1032px;
margin-top: var(--s-10);
text-align: left;
}
@media (max-width: 960px) { .features { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 560px) { .features { grid-template-columns: 1fr; } }
.feature {
position: relative; overflow: hidden;
background: var(--bg-1);
border: 1px solid var(--border);
border-radius: var(--r-lg);
padding: var(--s-5);
transition: transform var(--t-med) var(--ease), border-color var(--t-med), box-shadow var(--t-med);
}
.feature::after {
content: ""; position: absolute; inset: 0 0 auto; height: 1px;
background: linear-gradient(90deg, transparent, var(--fx, rgba(153,69,255,.5)), transparent);
opacity: 0; transition: opacity var(--t-med);
}
.feature:hover { transform: translateY(-2px); border-color: var(--border-strong); box-shadow: var(--shadow-2); }
.feature:hover::after { opacity: 1; }
.feature .fi {
width: 34px; height: 34px; border-radius: 9px;
display: grid; place-items: center;
margin-bottom: var(--s-3);
color: var(--fc, var(--brand-strong));
background: var(--fb, var(--brand-soft));
box-shadow: inset 0 0 0 1px var(--fr, var(--brand-border));
}
.feature h3 { margin: 0 0 5px; font-size: 14px; font-weight: 650; letter-spacing: -0.01em; }
.feature p { margin: 0; color: var(--text-2); font-size: 12.5px; line-height: 1.6; }
.feature.f-green { --fc: var(--green); --fb: var(--green-soft); --fr: rgba(52,211,153,.3); --fx: rgba(52,211,153,.45); }
.feature.f-cyan { --fc: var(--cyan); --fb: rgba(34,211,238,.10); --fr: rgba(34,211,238,.3); --fx: rgba(34,211,238,.45); }
.feature.f-amber { --fc: var(--amber); --fb: var(--amber-soft); --fr: rgba(251,191,36,.3); --fx: rgba(251,191,36,.45); }
/* footer */
.foot {
max-width: 1080px; margin: 0 auto;
padding: var(--s-5) var(--s-6);
border-top: 1px solid var(--border);
display: flex; align-items: center; gap: var(--s-4); flex-wrap: wrap;
color: var(--text-3); font-size: 12.5px;
}
.foot .sep { color: var(--border-strong); }
.foot a { color: var(--text-2); text-decoration: none; transition: color var(--t-fast); }
.foot a:hover { color: var(--text-1); }
.foot .spacer { flex: 1; }
/* ---------- status line ---------- */
.status { color: var(--text-2); font-size: 13.5px; margin: var(--s-6) auto; text-align: center; }
.status.err { color: var(--red); }
.spinner {
display: inline-block; width: 13px; height: 13px; margin-right: var(--s-2); vertical-align: -2px;
border: 2px solid var(--border-strong); border-top-color: var(--brand);
border-radius: var(--r-full); animation: spin .7s linear infinite;
}
/* ---------- cards ---------- */
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s-4); }
@media (max-width: 800px) { .grid { grid-template-columns: 1fr; } }
.card {
background: var(--bg-1);
border: 1px solid var(--border);
border-radius: var(--r-lg);
padding: var(--s-5) var(--s-6);
box-shadow: var(--shadow-1);
animation: fade-up .45s var(--ease) both;
}
.card.full { grid-column: 1 / -1; }
.card > h2 {
margin: 0 0 var(--s-4);
font-size: 11px; font-weight: 650;
text-transform: uppercase; letter-spacing: 0.09em;
color: var(--text-3);
display: flex; align-items: center; gap: var(--s-2);
}
.card > h2 .count {
font-family: var(--font-mono); font-weight: 500; font-size: 10.5px; letter-spacing: 0;
color: var(--text-2);
background: var(--bg-2); border: 1px solid var(--border);
border-radius: var(--r-full); padding: 1px 8px;
}
/* ---------- tx overview strip ---------- */
.overview {
background: var(--bg-1);
border: 1px solid var(--border);
border-radius: var(--r-lg);
box-shadow: var(--shadow-2);
overflow: hidden;
margin-bottom: var(--s-4);
animation: fade-up .45s var(--ease) both;
}
.ov-top { display: flex; align-items: center; gap: var(--s-5); padding: var(--s-5) var(--s-6); flex-wrap: wrap; }
.ov-status { display: flex; align-items: center; gap: var(--s-3); }
.ov-ring {
width: 42px; height: 42px; border-radius: var(--r-full);
display: grid; place-items: center; font-size: 19px; flex: 0 0 auto;
}
.ov-ring.ok { background: var(--green-soft); color: var(--green); box-shadow: inset 0 0 0 1px rgba(52,211,153,.35); }
.ov-ring.fail { background: var(--red-soft); color: var(--red); box-shadow: inset 0 0 0 1px rgba(248,113,113,.35); }
.ov-title { font-size: 17px; font-weight: 650; letter-spacing: -0.015em; }
.ov-sub { color: var(--text-2); font-size: 13px; margin-top: 1px; }
.ov-spacer { flex: 1; }
.ov-stats { display: flex; gap: var(--s-8); flex-wrap: wrap; }
.ov-stat .l { font-size: 10.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-3); }
.ov-stat .v { font-size: 15px; font-weight: 600; font-family: var(--font-mono); font-variant-numeric: tabular-nums; margin-top: 2px; }
.ov-progs { display: flex; gap: var(--s-2); flex-wrap: wrap; padding: 0 var(--s-6) var(--s-5); }
/* secondary metadata strip (fee payer, version, timestamp, blockhash…) */
.ov-meta {
display: flex; flex-wrap: wrap; gap: var(--s-4) var(--s-8);
padding: var(--s-4) var(--s-6);
border-top: 1px solid var(--border);
background: rgba(255,255,255,.014);
}
.ov-m { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.ov-m .l { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-3); }
.ov-m .v { font-size: 12.5px; color: var(--text-2); font-family: var(--font-mono); }
/* ---------- program chips / address pills ---------- */
.prog { display: inline-flex; align-items: center; gap: var(--s-2); min-width: 0; }
.prog .dot { width: 7px; height: 7px; border-radius: var(--r-full); flex: 0 0 auto; }
.prog .nm { font-weight: 550; font-size: 13px; white-space: nowrap; }
.prog .ad { color: var(--text-3); font-family: var(--font-mono); font-size: 11px; }
.pill {
display: inline-flex; align-items: center; gap: 7px;
background: var(--bg-2); border: 1px solid var(--border);
border-radius: var(--r-sm); padding: 5px var(--s-3); font-size: 12.5px;
}
.pill .dot { width: 6px; height: 6px; border-radius: var(--r-full); }
.pill .ad { color: var(--text-3); font-family: var(--font-mono); font-size: 10.5px; }
.addr-pill {
font-family: var(--font-mono); cursor: pointer;
border-radius: 6px; padding: 1px 5px; margin: -1px -5px;
transition: background var(--t-fast), color var(--t-fast);
}
.addr-pill:hover { background: var(--bg-2); color: var(--text-1); }
.addr-pill.copied { color: var(--green); }
/* ---------- CPI tree ---------- */
.tree { display: flex; flex-direction: column; }
.tnode { display: flex; align-items: center; gap: var(--s-3); padding: 6px var(--s-2); border-radius: var(--r-sm); }
.tnode:hover { background: var(--bg-2); }
.tnode .rail { color: var(--border-strong); font-family: var(--font-mono); font-size: 12px; white-space: pre; flex: 0 0 auto; }
.tnode .ix { color: var(--brand-strong); font-family: var(--font-mono); font-size: 11.5px; font-weight: 600; flex: 0 0 auto; width: 24px; }
.tnode .depth { font-size: 10.5px; color: var(--text-3); font-family: var(--font-mono); margin-left: auto; }
.tnode .ixname {
font-size: 11.5px; font-weight: 600; color: var(--brand-strong);
background: var(--brand-soft); border: 1px solid var(--brand-border);
border-radius: 6px; padding: 1px 8px; white-space: nowrap;
}
.tnode.expandable { cursor: pointer; }
.tnode .ix-caret {
color: var(--text-3); font-size: 9px; width: 10px; flex: 0 0 auto;
transition: transform var(--t-fast) var(--ease);
}
.tnode.on .ix-caret { transform: rotate(90deg); color: var(--brand-strong); }
/* expandable instruction detail */
.ixdetail {
display: none;
margin: 2px 0 8px 34px;
border-left: 2px solid var(--brand-border);
padding: var(--s-2) 0 var(--s-2) var(--s-4);
}
.ixdetail.open { display: block; animation: fade-up .2s var(--ease) both; }
.ixd-sec + .ixd-sec { margin-top: var(--s-3); }
.ixd-h {
font-size: 9.5px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.09em;
color: var(--text-3); margin-bottom: var(--s-2);
display: flex; align-items: center; gap: var(--s-2);
}
.ixd-n { font-family: var(--font-mono); font-weight: 500; color: var(--text-2);
background: var(--bg-2); border: 1px solid var(--border); border-radius: var(--r-full); padding: 0 6px; font-size: 9.5px; }
.ix-row { display: flex; align-items: baseline; gap: var(--s-4); padding: 3px 0; font-size: 12px; }
.ix-row + .ix-row { border-top: 1px solid var(--border); }
.ix-k, .ix-an { color: var(--text-2); flex: 0 0 auto; min-width: 170px; font-weight: 500; }
.ix-an { color: var(--cyan); font-weight: 550; }
.ix-an.dim { color: var(--text-3); }
.ix-ty { color: var(--text-3); font-family: var(--font-mono); font-size: 10px; font-weight: 400; }
.ix-v { font-family: var(--font-mono); font-variant-numeric: tabular-nums; color: var(--text-1); word-break: break-all; margin-left: auto; text-align: right; }
.ix-var { color: var(--text-3); font-style: italic; }
/* ---------- data rows (compute, balances, tokens) ---------- */
.rows { display: flex; flex-direction: column; }
.row { display: flex; align-items: center; gap: var(--s-4); padding: var(--s-2); border-radius: var(--r-sm); }
.row:hover { background: var(--bg-2); }
.row + .row { border-top: 1px solid var(--border); }
.row .grow { flex: 1; min-width: 0; }
.cu-bar {
height: 4px; border-radius: var(--r-full);
background: linear-gradient(90deg, var(--brand), var(--cyan));
margin-top: var(--s-2);
}
.cu-val { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: 12.5px; white-space: nowrap; }
.cu-val small { color: var(--text-3); }
.amt { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-weight: 600; white-space: nowrap; }
.amt.pos { color: var(--green); }
.amt.neg { color: var(--red); }
.amt-col { display: flex; flex-direction: column; align-items: flex-end; gap: 1px; }
.post-bal { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: 11px; color: var(--text-3); white-space: nowrap; }
.tok-sym { font-weight: 650; font-size: 13px; }
.tok-meta { color: var(--text-3); font-size: 11px; font-family: var(--font-mono); margin-top: 2px; }
.sub-owner { color: var(--text-2); font-size: 12px; }
.empty { color: var(--text-3); font-size: 13px; font-style: italic; padding: var(--s-2) 2px; }
.drift-note {
margin-bottom: var(--s-3);
padding: var(--s-3) var(--s-4);
border-radius: var(--r-md);
background: var(--amber-soft); border: 1px solid rgba(251,191,36,.28);
color: var(--text-2); font-size: 12.5px; line-height: 1.6;
}
.drift-note b { color: var(--text-1); }
.drift-note code { font-family: var(--font-mono); font-size: 11.5px; color: var(--amber); }
.drift-inline {
margin: var(--s-3) 0 0;
padding: 9px var(--s-3);
border-radius: var(--r-sm);
background: var(--amber-soft); border: 1px solid rgba(251,191,36,.24);
color: var(--text-2); font-size: 12px; line-height: 1.55;
}
.drift-inline b { color: var(--text-1); }
/* ---------- badges / replay ---------- */
.badge {
display: inline-flex; align-items: center; gap: 6px;
padding: 4px var(--s-3); border-radius: var(--r-full);
font-size: 12px; font-weight: 600;
}
.badge.ok { background: var(--green-soft); color: var(--green); }
.badge.fail { background: var(--red-soft); color: var(--red); }
.badge.neutral { background: var(--bg-2); color: var(--text-2); border: 1px solid var(--border); }
.b-dot { width: 6px; height: 6px; border-radius: var(--r-full); background: currentColor; }
.replay-head { display: flex; align-items: center; gap: var(--s-3); flex-wrap: wrap; }
.cu-note { color: var(--text-2); font-size: 12.5px; font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.cu-note b { color: var(--text-1); }
.err-box {
margin-top: var(--s-3);
background: var(--red-soft); border: 1px solid rgba(248,113,113,.28);
color: var(--red); border-radius: var(--r-md);
padding: var(--s-3) var(--s-4);
font-family: var(--font-mono); font-size: 12px; word-break: break-word;
}
/* ---------- log trace viewer ---------- */
.logs {
margin-top: var(--s-3);
background: var(--bg-inset);
border: 1px solid var(--border);
border-radius: var(--r-md);
padding: var(--s-4);
max-height: 320px; overflow: auto;
font-family: var(--font-mono); font-size: 12px; color: var(--text-2);
}
.logs .lg-line { white-space: pre-wrap; line-height: 1.8; display: flex; align-items: baseline; flex-wrap: wrap; }
.lg-dot { width: 6px; height: 6px; border-radius: var(--r-full); margin-right: 7px; align-self: center; }
.lg-prog { font-weight: 600; }
.lg-kw { color: var(--text-3); text-transform: uppercase; font-size: 9.5px; letter-spacing: 0.08em; margin: 0 6px; }
.lg-lvl { color: var(--text-3); }
.lg-ok { color: var(--green); font-weight: 600; }
.lg-fail { color: var(--red); }
.lg-inst { color: var(--cyan); font-weight: 600; }
.lg-key { color: var(--brand-strong); opacity: .9; }
.lg-msg { color: var(--text-2); }
.lg-dim { color: var(--text-3); }
/* ---------- address page (txlist + account overview) ---------- */
.txlist { max-width: 820px; margin: var(--s-2) auto 0; animation: fade-up .45s var(--ease) both; }
.txlist h2 {
font-size: 11px; font-weight: 650; text-transform: uppercase; letter-spacing: 0.09em;
color: var(--text-3); margin: var(--s-5) 2px var(--s-3);
}
.txlist h2 b { color: var(--text-1); }
.txrow {
display: flex; align-items: center; gap: var(--s-3);
padding: var(--s-3) var(--s-4); margin: var(--s-2) 0;
background: var(--bg-1); border: 1px solid var(--border); border-radius: var(--r-md);
cursor: pointer;
transition: border-color var(--t-fast), background var(--t-fast);
}
.txrow:hover { border-color: var(--brand-border); background: var(--bg-2); }
.txrow .st { width: 7px; height: 7px; border-radius: var(--r-full); flex: 0 0 auto; }
.txrow .st.ok { background: var(--green); }
.txrow .st.err { background: var(--red); }
.txrow .sig {
font-family: var(--font-mono); font-size: 12.5px; color: var(--text-1);
flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.txrow .slot { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: 11.5px; color: var(--text-3); white-space: nowrap; }
.txrow .when { font-size: 12px; color: var(--text-2); white-space: nowrap; }
.txrow .go { color: var(--brand-strong); font-size: 12.5px; font-weight: 600; white-space: nowrap; opacity: 0; transition: opacity var(--t-fast); }
.txrow:hover .go { opacity: 1; }
.acct-ov {
background: var(--bg-1); border: 1px solid var(--border); border-radius: var(--r-lg);
box-shadow: var(--shadow-2);
padding: var(--s-6); margin-bottom: var(--s-5);
}
.acct-ov .ov-tag {
display: inline-block;
background: var(--brand-soft); color: var(--brand-strong);
border: 1px solid var(--brand-border);
padding: 3px 10px; border-radius: var(--r-full);
font-size: 11px; font-weight: 600; letter-spacing: 0.02em;
}
.acct-ov .ov-name { font-size: 22px; font-weight: 700; letter-spacing: -0.02em; margin: var(--s-3) 0 var(--s-4); }
.acct-ov .ov-sub { color: var(--text-2); font-size: 13px; }
.acct-ov .ov-row {
display: flex; justify-content: space-between; gap: var(--s-4);
padding: 10px 0; border-top: 1px solid var(--border);
font-size: 13px;
}
.acct-ov .ov-row:first-of-type { border-top: 0; }
.acct-ov .ovk { color: var(--text-2); }
.acct-ov .ovoff { color: var(--text-3); font-family: var(--font-mono); font-size: 10.5px; }
.acct-ov .ovv { color: var(--text-1); font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: 12.5px; text-align: right; }
.acct-ov .ov-fields-label {
font-size: 10.5px; font-weight: 650; text-transform: uppercase; letter-spacing: 0.08em;
color: var(--text-3); margin: var(--s-4) 0 var(--s-1);
}
/* ---------- what-if: account editor ---------- */
.whatif-lead { color: var(--text-2); font-size: 13.5px; line-height: 1.6; margin: 0 0 var(--s-4); max-width: 680px; }
.editor-toolbar { display: flex; gap: var(--s-3); align-items: center; margin-bottom: var(--s-3); }
.editor-toolbar .filter {
flex: 1;
background: var(--bg-inset); border: 1px solid var(--border); color: var(--text-1);
padding: 9px var(--s-3); border-radius: var(--r-md);
font-family: var(--font-mono); font-size: 12.5px;
transition: border-color var(--t-fast), box-shadow var(--t-fast);
}
.editor-toolbar .filter::placeholder { color: var(--text-3); }
.editor-toolbar .filter:focus { outline: none; border-color: var(--brand-border); box-shadow: var(--ring); }
.editor-toolbar .summary { color: var(--text-3); font-size: 12.5px; white-space: nowrap; }
.acct-card {
border: 1px solid var(--border); border-radius: var(--r-md);
background: var(--bg-0);
margin: var(--s-2) 0; overflow: hidden;
transition: border-color var(--t-fast);
}
.acct-card.open { border-color: var(--border-strong); }
.acct-card.has-edits { border-color: rgba(251,191,36,.5); }
.acct-head {
display: flex; align-items: center; gap: var(--s-3);
width: 100%; text-align: left;
background: none; border: 0; color: var(--text-1);
padding: var(--s-3) var(--s-4);
cursor: pointer; font-family: var(--font-sans); font-size: 13px;
transition: background var(--t-fast);
}
.acct-head:hover { background: var(--bg-2); }
.acct-head .chev { color: var(--text-3); font-size: 10px; width: 10px; flex: 0 0 auto; transition: transform var(--t-fast); }
.acct-card.open .acct-head .chev { transform: rotate(90deg); }
.acct-head .edit-dot { width: 7px; height: 7px; border-radius: var(--r-full); background: var(--amber); flex: 0 0 auto; }
.acct-head .hint { color: var(--text-2); font-size: 12px; font-family: var(--font-mono); font-variant-numeric: tabular-nums; margin-left: auto; white-space: nowrap; }
.acct-body { padding: var(--s-1) var(--s-4) var(--s-4); border-top: 1px solid var(--border); }
.type-tag {
background: var(--brand-soft); color: var(--brand-strong);
border: 1px solid var(--brand-border);
padding: 2px 9px; border-radius: var(--r-full);
font-size: 11px; font-weight: 600; white-space: nowrap;
}
.type-tag.raw { background: var(--bg-2); color: var(--text-3); border-color: var(--border); }
.type-tag.mint { background: var(--green-soft); color: var(--green); border-color: rgba(52,211,153,.35); }
.type-tag.sys { background: rgba(34,211,238,.10); color: var(--cyan); border-color: rgba(34,211,238,.3); }
.acct-meta { display: flex; gap: var(--s-2); flex-wrap: wrap; align-items: center; margin: var(--s-3) 0 var(--s-1); font-size: 12.5px; color: var(--text-2); }
table.fields { width: 100%; border-collapse: collapse; margin-top: var(--s-3); }
table.fields th {
text-align: left; padding: 0 var(--s-3) var(--s-2);
font-size: 10.5px; font-weight: 650; text-transform: uppercase; letter-spacing: 0.07em;
color: var(--text-3); border-bottom: 1px solid var(--border);
}
table.fields td { padding: 10px var(--s-3); border-bottom: 1px solid var(--border); vertical-align: middle; }
table.fields tr:last-child td { border-bottom: 0; }
.f-name { font-family: var(--font-mono); font-size: 12.5px; color: var(--text-1); }
.f-off { color: var(--text-3); font-size: 11px; font-family: var(--font-mono); }
.f-type {
font-family: var(--font-mono); font-size: 10.5px; color: var(--text-2);
background: var(--bg-2); border: 1px solid var(--border);
border-radius: 5px; padding: 1px 6px; white-space: nowrap;
}
.f-cur { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: 12px; color: var(--text-2); word-break: break-all; }
.f-cur.ro { color: var(--text-3); }
input.f-in {
width: 100%; min-width: 120px;
background: var(--bg-inset); border: 1px solid var(--border); color: var(--text-1);
padding: 7px 10px; border-radius: var(--r-sm);
font-family: var(--font-mono); font-size: 12px;
transition: border-color var(--t-fast), box-shadow var(--t-fast);
}
input.f-in:focus { outline: none; border-color: var(--brand-border); box-shadow: var(--ring); }
input.f-in.dirty { border-color: rgba(251,191,36,.55); box-shadow: 0 0 0 3px rgba(251,191,36,.14); }
.ro-dash { color: var(--text-3); font-family: var(--font-mono); font-size: 12px; }
.raw-note {
background: var(--bg-1); border: 1px dashed var(--border-strong); border-radius: var(--r-md);
padding: var(--s-4); margin-top: var(--s-3);
color: var(--text-2); font-size: 13px;
}
.raw-inputs { display: flex; gap: var(--s-2); margin-top: var(--s-3); }
.raw-inputs input {
background: var(--bg-inset); border: 1px solid var(--border); color: var(--text-1);
padding: 8px 10px; border-radius: var(--r-sm);
font-family: var(--font-mono); font-size: 12px;
}
.raw-inputs input:focus { outline: none; border-color: var(--brand-border); box-shadow: var(--ring); }
.editor-actions { display: flex; gap: var(--s-2); margin-top: var(--s-4); align-items: center; flex-wrap: wrap; }
.editor-actions .spacer { flex: 1; }
.dirty-count { color: var(--amber); font-size: 12px; font-family: var(--font-mono); }
/* ---------- scenario tests ---------- */
.suite-toolbar { display: flex; align-items: center; gap: var(--s-3); margin-bottom: var(--s-3); flex-wrap: wrap; }
.suite-toolbar .spacer { flex: 1; }
.suite-result { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: 13px; font-weight: 600; }
.suite-result.pass { color: var(--green); }
.suite-result.fail { color: var(--red); }
.suite { display: flex; flex-direction: column; gap: var(--s-2); }
.srow {
border: 1px solid var(--border); border-left: 3px solid var(--border-strong);
border-radius: var(--r-md);
background: var(--bg-0); overflow: hidden;
transition: border-color var(--t-fast);
}
.srow.pass { border-left-color: var(--green); }
.srow.fail { border-left-color: var(--red); }
.srow-head { display: flex; align-items: center; gap: var(--s-3); padding: var(--s-3) var(--s-4); }
.srow .st { width: 20px; flex: 0 0 auto; text-align: center; font-size: 14px; }
.srow .st .pend { color: var(--text-3); }
.srow .info { flex: 1; min-width: 0; cursor: pointer; }
.srow .nm { font-weight: 600; font-size: 13.5px; }
.srow .ch { color: var(--text-3); font-family: var(--font-mono); font-size: 11.5px; margin-top: 2px; }
.srow .expect-sel { display: flex; align-items: center; gap: var(--s-2); flex: 0 0 auto; }
.srow .expect-sel label { font-size: 10.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-3); }
.srow select.exp {
background: var(--bg-inset); border: 1px solid var(--border); color: var(--text-1);
padding: 5px 8px; border-radius: var(--r-sm);
font-size: 12px; font-family: var(--font-sans); cursor: pointer;
}
.srow select.exp:focus { outline: none; border-color: var(--brand-border); }
.srow input.contains {
background: var(--bg-inset); border: 1px solid var(--border); color: var(--text-1);
padding: 5px 8px; border-radius: var(--r-sm);
font-size: 11.5px; font-family: var(--font-mono); width: 130px;
}
.srow input.contains:focus { outline: none; border-color: var(--brand-border); }
.srow .got {
font-family: var(--font-mono); font-size: 11.5px; color: var(--text-2);
flex: 0 0 auto; max-width: 260px;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.srow .rm { background: none; border: 0; color: var(--text-3); cursor: pointer; font-size: 15px; padding: 0 2px; flex: 0 0 auto; transition: color var(--t-fast); }
.srow .rm:hover { color: var(--red); }
.srow-logs { border-top: 1px solid var(--border); padding: var(--s-3) var(--s-4); background: var(--bg-inset); }
.export-box { margin-top: var(--s-3); }
.export-box pre {
background: var(--bg-inset); border: 1px solid var(--border); border-radius: var(--r-md);
padding: var(--s-4);
overflow: auto; max-height: 320px;
font-family: var(--font-mono); font-size: 11.5px; color: var(--text-2);
margin: 0;
}
.export-box .cmd { color: var(--green); font-family: var(--font-mono); font-size: 12px; margin: var(--s-2) 0 0; line-height: 1.8; }
.staged-item {
display: flex; align-items: center; gap: var(--s-3);
padding: var(--s-2) var(--s-3); margin: var(--s-2) 0;
background: var(--bg-0); border: 1px solid var(--border); border-left: 3px solid var(--amber);
border-radius: var(--r-sm);
font-family: var(--font-mono); font-size: 12px;
}
.staged-item .k { color: var(--amber); font-weight: 600; }
.staged-item .body { flex: 1; color: var(--text-2); word-break: break-all; }
.staged-item .rm { background: none; border: 0; color: var(--text-3); cursor: pointer; font-size: 15px; padding: 0 4px; }
.staged-item .rm:hover { color: var(--red); }
/* ---------- verdict + comparison ---------- */
.sim-result { margin-top: var(--s-4); }
.verdict {
display: flex; gap: var(--s-4); align-items: flex-start;
padding: var(--s-4) var(--s-5); border-radius: var(--r-md); margin-bottom: var(--s-3);
}
.verdict.ok { background: var(--green-soft); border: 1px solid rgba(52,211,153,.3); }
.verdict.bad { background: var(--red-soft); border: 1px solid rgba(248,113,113,.3); }
.verdict .ic { font-size: 22px; line-height: 1; flex: 0 0 auto; }
.verdict .vt { font-weight: 700; font-size: 15.5px; letter-spacing: -0.01em; }
.verdict .vs { color: var(--text-2); font-size: 13px; margin-top: 3px; line-height: 1.55; }
.compare { display: grid; grid-template-columns: 1fr auto 1fr; gap: var(--s-4); align-items: stretch; margin-bottom: var(--s-3); }
@media (max-width: 560px) { .compare { grid-template-columns: 1fr; } .compare .arrow { display: none; } }
.compare-col { background: var(--bg-0); border: 1px solid var(--border); border-radius: var(--r-md); padding: var(--s-4); }
.compare-col .lbl { font-size: 10px; font-weight: 650; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-3); margin-bottom: var(--s-2); }
.compare .arrow { color: var(--text-3); font-size: 18px; display: grid; place-items: center; }
.delta { font-size: 11.5px; margin-top: var(--s-2); font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.delta.up { color: var(--green); }
.delta.down { color: var(--red); }
.delta.same { color: var(--text-3); }
/* ---------- advanced disclosure ---------- */
details.advanced { margin-top: var(--s-6); border-top: 1px solid var(--border); padding-top: var(--s-2); }
details.advanced > summary {
cursor: pointer; list-style: none; user-select: none;
display: flex; align-items: center; gap: var(--s-2);
padding: var(--s-3) 2px;
color: var(--text-2); font-size: 13px; font-weight: 550;
transition: color var(--t-fast);
}
details.advanced > summary::-webkit-details-marker { display: none; }
details.advanced > summary::before { content: '▸'; color: var(--text-3); transition: transform var(--t-fast); }
details.advanced[open] > summary::before { transform: rotate(90deg); }
details.advanced > summary:hover { color: var(--text-1); }
details.advanced .adv-body { padding-top: var(--s-2); }
/* ---------- pre-flight simulator ---------- */
.hero-cta { margin-top: var(--s-6); }
.simview { animation: fade-up .45s var(--ease) both; }
.tabs { display: flex; gap: var(--s-1); margin: var(--s-4) 0 var(--s-5); border-bottom: 1px solid var(--border); }
.tab {
appearance: none; background: none; border: 0; cursor: pointer;
padding: var(--s-3) var(--s-4); margin-bottom: -1px;
color: var(--text-3); font-family: var(--font-sans); font-size: 13.5px; font-weight: 550;
border-bottom: 2px solid transparent;
transition: color var(--t-fast), border-color var(--t-fast);
}
.tab:hover { color: var(--text-2); }
.tab.on { color: var(--text-1); border-bottom-color: var(--brand); }
.flabel {
display: block; margin-bottom: var(--s-2);
font-size: 10.5px; font-weight: 650; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-3);
}
.ta {
width: 100%; min-height: 130px; resize: vertical;
background: var(--bg-inset); border: 1px solid var(--border); color: var(--text-1);
padding: var(--s-3) var(--s-4); border-radius: var(--r-md);
font-family: var(--font-mono); font-size: 12px; line-height: 1.6;
transition: border-color var(--t-fast), box-shadow var(--t-fast);
}
.ta::placeholder { color: var(--text-3); }
.ta:focus { outline: none; border-color: var(--brand-border); box-shadow: var(--ring); }
.finput {
width: 100%;
background: var(--bg-inset); border: 1px solid var(--border); color: var(--text-1);
padding: 9px var(--s-3); border-radius: var(--r-md);
font-family: var(--font-sans); font-size: 13px;
transition: border-color var(--t-fast), box-shadow var(--t-fast);
}
.finput.mono { font-family: var(--font-mono); font-size: 12.5px; }
.finput::placeholder { color: var(--text-3); }
.finput:focus { outline: none; border-color: var(--brand-border); box-shadow: var(--ring); }
.row-inline { display: flex; gap: var(--s-2); align-items: center; }
.ixdoc { color: var(--text-2); font-size: 12.5px; margin: var(--s-3) 0 0; line-height: 1.55; }
.ixsec { font-size: 10.5px; font-weight: 650; text-transform: uppercase; letter-spacing: .08em;
color: var(--text-3); margin: var(--s-5) 0 var(--s-2); }
.ixrow { display: grid; grid-template-columns: 190px 1fr; gap: var(--s-3); align-items: center; margin-bottom: var(--s-2); }
@media (max-width: 620px) { .ixrow { grid-template-columns: 1fr; } }
.ixrow .nm { font-family: var(--font-mono); font-size: 12.5px; }
.ixrow .tags { display: inline-flex; gap: 5px; margin-left: 6px; }
.ixtag { font-size: 9.5px; font-weight: 650; padding: 1px 5px; border-radius: 4px; letter-spacing: .04em;
background: var(--bg-2); color: var(--text-3); border: 1px solid var(--border); }
.ixtag.s { color: var(--amber); border-color: rgba(251,191,36,.35); background: var(--amber-soft); }
.ixtag.w { color: var(--cyan); border-color: rgba(34,211,238,.3); background: rgba(34,211,238,.1); }
input.derived { border-color: rgba(52,211,153,.45); color: var(--green); }
.buildhint { margin-top: var(--s-4); color: var(--text-3); font-size: 12.5px; line-height: 1.6; }
.tryrow { margin-top: var(--s-3); display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap; }
.trychip {
appearance: none; cursor: pointer;
background: var(--brand-soft); border: 1px solid var(--brand-border); color: var(--brand-strong);
padding: 6px var(--s-3); border-radius: var(--r-full);
font-family: var(--font-sans); font-size: 12.5px; font-weight: 550;
transition: filter var(--t-fast);
}
.trychip:hover { filter: brightness(1.15); }
.trychip .dim { color: var(--text-3); font-weight: 400; }
/* ---------- global clock control (header) ---------- */
.clockctl { position: relative; flex: 0 0 auto; }
.clockbtn {
display: inline-flex; align-items: center; gap: 6px;
font-family: var(--font-sans); font-weight: 550; white-space: nowrap;
}
.clockbtn.warped { border-color: var(--brand-border); color: var(--brand-strong); background: var(--brand-soft); }
.clockpop {
position: absolute; top: calc(100% + 8px); right: 0; z-index: 50;
width: 340px; padding: var(--s-4) var(--s-5);
background: var(--bg-1); border: 1px solid var(--border-strong);
border-radius: var(--r-lg); box-shadow: var(--shadow-3);
}
.cp-title { font-size: 13px; font-weight: 650; margin-bottom: var(--s-1); }
.cp-sub { color: var(--text-3); font-size: 12px; line-height: 1.5; margin-bottom: var(--s-3); }
.cp-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--s-2); }
.cp-grid .chip { width: 100%; padding: 6px 4px; font-size: 11.5px; text-align: center; }
.cp-custom { display: flex; gap: var(--s-2); margin-top: var(--s-3); }
.cp-custom .finput { padding: 7px var(--s-2); font-size: 12.5px; }
.cp-custom #ttAmount { flex: 0 0 96px; width: 96px; min-width: 0; font-family: var(--font-mono); }
.cp-custom #ttUnit { flex: 1 1 auto; min-width: 0; }
.cp-state {
margin-top: var(--s-3); padding-top: var(--s-3); border-top: 1px solid var(--border);
color: var(--text-2); font-family: var(--font-mono); font-size: 11.5px; line-height: 1.5;
}
.cp-state.warped { color: var(--brand-strong); }
/* a page-wide banner so you never forget the clock is shifted */
.clockbanner {
display: flex; align-items: center; gap: var(--s-3);
background: var(--brand-soft); border: 1px solid var(--brand-border);
border-radius: var(--r-md); padding: var(--s-3) var(--s-4); margin-bottom: var(--s-4);
font-size: 13px; color: var(--brand-strong);
}
.clockbanner .rst {
margin-left: auto; background: none; border: 0; cursor: pointer;
color: var(--text-2); font-family: var(--font-sans); font-size: 12.5px; text-decoration: underline;
}
.clockbanner .rst:hover { color: var(--text-1); }
/* feature-gate toggle list inside the Features popover */
/* wider popover so gate names fit on one line */
#featPop { width: 440px; }
#featPop .cp-custom #featCustom { flex: 1 1 auto; min-width: 0; }
#featPop .cp-custom #featCustomState { flex: 0 0 auto; width: auto; }
.feat-list { display: flex; flex-direction: column; max-height: 330px; overflow-y: auto; margin: 0 -4px; padding: 0 4px; }
.feat-head { font-size: 10px; font-weight: 700; letter-spacing: .7px; text-transform: uppercase; color: var(--text-3); margin: 10px 2px 4px; }
.feat-head:first-child { margin-top: 2px; }
.feat-row {
display: flex; align-items: center; gap: 12px; padding: 8px 8px; cursor: pointer;
border-radius: var(--r-sm); border: 1px solid transparent;
}
.feat-row:hover { background: var(--surface-2); }
.feat-main { display: flex; flex-direction: column; gap: 1px; flex: 1; min-width: 0; }
.feat-name {
font-size: 12.5px; font-weight: 600; color: var(--text-1);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.feat-note {
font-size: 11px; color: var(--text-3);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.feat-row.off .feat-name { color: var(--text-2); }
/* iOS-style on/off toggle */
.switch { position: relative; flex: 0 0 auto; width: 36px; height: 21px; }
.switch input { position: absolute; inset: 0; opacity: 0; margin: 0; cursor: pointer; z-index: 1; }
.slider {
position: absolute; inset: 0; border-radius: 999px; background: var(--line-2);
transition: background .16s ease;
}
.slider::before {
content: ''; position: absolute; top: 2.5px; left: 2.5px; width: 16px; height: 16px;
border-radius: 50%; background: #fff; box-shadow: 0 1px 2px rgba(0,0,0,.35); transition: transform .16s ease;
}
.switch input:checked + .slider { background: var(--brand-strong); }
.switch input:checked + .slider::before { transform: translateX(15px); }
/* ---------- time travel ---------- */
.timetravel {
margin-top: var(--s-5);
background: var(--bg-0);
border: 1px solid var(--border);
border-radius: var(--r-md);
padding: var(--s-4) var(--s-5);
}
.tt-head { display: flex; align-items: baseline; gap: var(--s-3); flex-wrap: wrap; margin-bottom: var(--s-3); }
.tt-title { font-size: 13px; font-weight: 650; }
.tt-sub { color: var(--text-3); font-size: 12.5px; line-height: 1.5; }
.tt-row { display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap; }
.chip {
appearance: none; cursor: pointer;
background: var(--bg-2); border: 1px solid var(--border); color: var(--text-2);
padding: 6px var(--s-3); border-radius: var(--r-full);
font-family: var(--font-sans); font-size: 12.5px; font-weight: 550;
transition: border-color var(--t-fast), color var(--t-fast), background var(--t-fast);
}
.chip:hover { color: var(--text-1); border-color: var(--border-strong); }
.chip.on { background: var(--brand-soft); border-color: var(--brand-border); color: var(--brand-strong); }
.tt-custom { display: inline-flex; align-items: center; gap: var(--s-2); margin-left: var(--s-2); }
.tt-custom .finput { width: auto; }
.tt-custom #ttAmount { width: 84px; font-family: var(--font-mono); }
.tt-custom #ttUnit { width: auto; padding-right: var(--s-4); }
.tt-state {
margin-top: var(--s-3); padding-top: var(--s-3);
border-top: 1px solid var(--border);
color: var(--text-2); font-family: var(--font-mono); font-size: 12px;
}
.tt-state.warped { color: var(--brand-strong); }
/* ---------- explanation + account diff ---------- */
.explain {
display: flex; gap: var(--s-4); align-items: flex-start;
background: var(--red-soft); border: 1px solid rgba(248,113,113,.32);
border-radius: var(--r-md); padding: var(--s-4) var(--s-5); margin-bottom: var(--s-4);
}
.explain .ic { font-size: 20px; line-height: 1.2; flex: 0 0 auto; }
.explain .t { font-weight: 700; font-size: 15px; color: var(--red); letter-spacing: -0.01em; }
.explain .d { color: var(--text-1); font-size: 13.5px; margin-top: 3px; line-height: 1.55; }
.explain .meta { color: var(--text-3); font-family: var(--font-mono); font-size: 11px; margin-top: var(--s-2); }
.diffs { display: flex; flex-direction: column; gap: var(--s-2); }
.dcard { background: var(--bg-0); border: 1px solid var(--border); border-radius: var(--r-md); padding: var(--s-3) var(--s-4); }
.dhead { display: flex; align-items: center; gap: var(--s-3); flex-wrap: wrap; }
.dhead .lam { margin-left: auto; font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: 12px; }
.drow { display: grid; grid-template-columns: 160px 1fr; gap: var(--s-3); align-items: baseline;
padding: 6px 0; border-top: 1px solid var(--border); font-family: var(--font-mono); font-size: 12px; }
.drow:first-of-type { margin-top: var(--s-2); }
.drow .fname { color: var(--text-2); }
.drow .fv { display: flex; align-items: baseline; gap: var(--s-2); flex-wrap: wrap; }
.drow .old { color: var(--text-3); text-decoration: line-through; }
.drow .arr { color: var(--text-3); }
.drow .new { color: var(--green); font-weight: 600; word-break: break-all; }
.hidden { display: none !important; }
/* aliases for inline styles in templates */
:root { --muted: var(--text-3); --text: var(--text-1); --line-2: var(--border-strong); }
</style>
</head>
<body>
<div class="aurora" aria-hidden="true"><b class="a1"></b><b class="a2"></b><b class="a3"></b></div>
<div id="spot" aria-hidden="true"></div>
<div class="topbar">
<div class="topbar-inner">
<div class="logo" id="brand" title="Home">
<div class="mark" aria-hidden="true">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="6.4" stroke="#fff" stroke-width="2"/>
<circle cx="12" cy="12" r="2.1" fill="#fff"/>
<path d="M12 1.6v3.6M12 18.8v3.6M1.6 12h3.6M18.8 12h3.6" stroke="#fff" stroke-width="2" stroke-linecap="round"/>
</svg>
</div>
<div><b>svmscope</b></div>
</div>
<nav class="nav">
<button class="navtab on" id="navAnalyze">Analyze</button>
<button class="navtab" id="navSim">⚡ Simulate</button>
</nav>
<div class="search">
<select id="cluster" class="cluster" title="Cluster / RPC">
<option value="mainnet">Mainnet</option>
<option value="devnet">Devnet</option>
<option value="testnet">Testnet</option>
<option value="localnet">Localnet</option>
<option value="custom">Custom RPC…</option>
</select>
<input id="customRpc" class="cluster customrpc hidden" placeholder="https://your-rpc…" spellcheck="false" autocomplete="off" title="Custom RPC endpoint" />
<!-- Global clock: everything on the page simulates at this point in time -->
<div class="clockctl">
<button class="cluster clockbtn" id="clockBtn" title="Simulate at a different point in time">
<span id="clockLabel">⏱ Now</span>
</button>
<div class="clockpop hidden" id="clockPop">
<div class="cp-title">Simulate at</div>
<div class="cp-sub">Applies everywhere — replays, what-ifs and pre-flight all run at this time.
Test lockups, vesting cliffs, cooldowns, reward accrual, auctions.</div>
<div class="cp-grid">
<button class="chip gtt on" data-tt='{}'>Now</button>
<button class="chip gtt" data-tt='{"epochs":1}'>+1 epoch</button>
<button class="chip gtt" data-tt='{"seconds":3600}'>+1 hour</button>
<button class="chip gtt" data-tt='{"seconds":86400}'>+1 day</button>
<button class="chip gtt" data-tt='{"seconds":604800}'>+1 week</button>
<button class="chip gtt" data-tt='{"seconds":2592000}'>+30 days</button>
<button class="chip gtt" data-tt='{"seconds":7776000}'>+90 days</button>
<button class="chip gtt" data-tt='{"seconds":31536000}'>+1 year</button>
</div>
<div class="cp-custom">
<input id="ttAmount" class="finput" type="number" min="0" placeholder="0" />
<select id="ttUnit" class="finput">
<option value="minutes">minutes</option>
<option value="hours">hours</option>
<option value="days" selected>days</option>
<option value="epochs">epochs</option>
<option value="slots">slots</option>
</select>
<button class="btn sm" id="ttApply">Set</button>
</div>
<div id="ttState" class="cp-state">Simulating at the current time.</div>
</div>
</div>
<div class="clockctl">
<button class="cluster clockbtn" id="featBtn" title="Replay with Solana feature gates toggled on/off">
<span id="featLabel">⚑ Features</span>
</button>
<div class="clockpop hidden" id="featPop">
<div class="cp-title">Feature gates</div>
<div class="cp-sub">Replay as if a Solana runtime feature were (in)active. Most execution-affecting
SIMDs ship on-chain as one of these — flip one and re-run to see if a transaction still works.</div>
<input id="featSearch" class="finput" placeholder="search all 240 active gates…" spellcheck="false" style="margin-bottom:8px" />
<div id="featList" class="feat-list"></div>
<div class="cp-custom" style="margin-top:10px">
<input id="featCustom" class="finput mono" placeholder="feature gate pubkey…" spellcheck="false" style="flex:1" />
<select id="featCustomState" class="finput"><option value="on">activate</option><option value="off">deactivate</option></select>
<button class="btn sm" id="featAdd">Add</button>
</div>
<div id="featState" class="cp-state">Running under mainnet's active feature set.</div>
</div>
</div>
<input id="sig" placeholder="paste a transaction signature — or a program / account address…" spellcheck="false" autocomplete="off" />
<button class="btn" id="go">Analyze</button>
</div>
</div>
</div>
<main>
<div id="clockBanner" class="clockbanner hidden"></div>
<div id="featBanner" class="clockbanner hidden"></div>
<div id="hero" class="hero">
<div class="eyebrow"><span class="bkt">[</span><span class="pulse"></span> the Solana transaction simulation layer <span class="bkt">]</span></div>
<h1>
<span class="h1-line"><span class="h1-inner">Decode, replay</span></span>
<span class="h1-line"><span class="h1-inner">& <span class="serif grad">mutate</span> any</span></span>
<span class="h1-line"><span class="h1-inner"><span class="hollow">transaction</span></span></span>
</h1>
<p>Reconstruct the exact state a Solana transaction ran against, re-execute the real
programs locally, then rewrite an account's fields to ask <span class="serif">“what if?”</span></p>
<div class="hero-search">
<span class="hs-icon" aria-hidden="true">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none">
<circle cx="11" cy="11" r="7" stroke="currentColor" stroke-width="2"/>
<path d="M20 20l-3.5-3.5" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</span>
<input id="heroSig" placeholder="paste a transaction signature — or a program / account address" spellcheck="false" autocomplete="off" />
<button class="btn" id="heroGo">Analyze</button>
</div>
<div class="examples">
<span class="lbl">or dissect a live one:</span>
<button class="ex-chip" data-ex="JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4"><span class="dot" style="background:#fb923c"></span>Jupiter swap</button>
<button class="ex-chip" data-ex="pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA"><span class="dot" style="background:#34d399"></span>Pump AMM</button>
<button class="ex-chip" data-ex="whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc"><span class="dot" style="background:#22d3ee"></span>Orca Whirlpool</button>
</div>
<div class="hint">press <kbd>/</kbd> to search anytime</div>
<div class="term-shell reveal">
<div class="term" id="termTilt">
<div class="term-bar" aria-hidden="true">
<span class="td"></span><span class="td"></span><span class="td"></span>
<span class="tt">replay — svmscope</span>
</div>
<div class="term-body" id="termBody" aria-hidden="true"></div>
</div>
</div>
<div class="ticker reveal" aria-hidden="true">
<div class="ticker-track">
<span class="ti"><b class="br">Program JUP6…TaV4</b> invoke [1]</span><span class="sep">◆</span>
<span class="ti">consumed <b>178,113</b> of 200,000 CU</span><span class="sep">◆</span>
<span class="ti">REPLAY: <b class="ok">success ✓</b></span><span class="sep">◆</span>
<span class="ti"><b>22 accounts</b> + <b>6 program ELFs</b> reconstructed</span><span class="sep">◆</span>
<span class="ti"><b class="cy">pool.reserve</b> → 0</span><span class="sep">◆</span>
<span class="ti"><b class="bad">Error: SlippageExceeded</b></span><span class="sep">◆</span>
<span class="ti">clock warped <b class="cy">+30 days</b></span><span class="sep">◆</span>
<span class="ti">claim: <b class="bad">fails now</b> · <b class="ok">works after the cliff</b></span><span class="sep">◆</span>
<span class="ti">PDAs derived <b>in your browser</b></span><span class="sep">◆</span>
<span class="ti">MUTATED REPLAY: <b class="ok">success ✓</b></span><span class="sep">◆</span>
<span class="ti"><b class="br">Program JUP6…TaV4</b> invoke [1]</span><span class="sep">◆</span>
<span class="ti">consumed <b>178,113</b> of 200,000 CU</span><span class="sep">◆</span>
<span class="ti">REPLAY: <b class="ok">success ✓</b></span><span class="sep">◆</span>
<span class="ti"><b>22 accounts</b> + <b>6 program ELFs</b> reconstructed</span><span class="sep">◆</span>
<span class="ti"><b class="cy">pool.reserve</b> → 0</span><span class="sep">◆</span>
<span class="ti"><b class="bad">Error: SlippageExceeded</b></span><span class="sep">◆</span>
<span class="ti">clock warped <b class="cy">+30 days</b></span><span class="sep">◆</span>
<span class="ti">claim: <b class="bad">fails now</b> · <b class="ok">works after the cliff</b></span><span class="sep">◆</span>
<span class="ti">PDAs derived <b>in your browser</b></span><span class="sep">◆</span>
<span class="ti">MUTATED REPLAY: <b class="ok">success ✓</b></span><span class="sep">◆</span>
</div>
</div>
<div class="features">
<div class="feature reveal">
<div class="fi" aria-hidden="true">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<path d="M6 4v10a3 3 0 0 0 3 3h9"/><circle cx="6" cy="4" r="1.6" fill="currentColor" stroke="none"/>
<circle cx="19" cy="17" r="1.6" fill="currentColor" stroke="none"/><path d="M6 9h7"/><circle cx="14.5" cy="9" r="1.6" fill="currentColor" stroke="none"/>
</svg>
</div>
<h3>Decode</h3>
<p>The full cross-program call tree, balance flows and compute per program — lookup tables resolved, accounts named via on-chain IDLs.</p>
</div>
<div class="feature reveal f-green">
<div class="fi" aria-hidden="true">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 5v14l16-7z" fill="currentColor" stroke="none"/>
</svg>
</div>
<h3>Replay locally</h3>
<p>Rebuilds the world the transaction ran in — every account and program binary — and re-executes the real code in an embedded SVM.</p>
</div>
<div class="feature reveal f-cyan">
<div class="fi" aria-hidden="true">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<path d="M4 8h10M18 8h2M4 16h2M10 16h10"/><circle cx="16" cy="8" r="2.2"/><circle cx="7" cy="16" r="2.2"/>
</svg>
</div>
<h3>Ask “what if?”</h3>
<p>Rewrite a pool reserve, an oracle price, any account's bytes — then replay and diff to see what would have happened instead.</p>
</div>
<div class="feature reveal f-amber">
<div class="fi" aria-hidden="true">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<circle cx="12" cy="12" r="8.2"/><path d="M12 7.5V12l3 2.4"/>
</svg>
</div>
<h3>Time travel</h3>
<p>Warp the clock a day, an epoch, a year — and watch vesting cliffs, cooldowns and auctions play out without waiting.</p>
</div>
</div>
<div class="hero-cta reveal">
<button class="btn ghost" id="openSim">⚡ Simulate a transaction before sending it</button>
</div>
</div>
<div id="status" class="status hidden"></div>
<div id="txlist" class="txlist hidden"></div>
<!-- Pre-flight: simulate an unsigned transaction, or build one from an IDL -->
<div id="sim" class="simview hidden">
<div class="card full">
<h2>Pre-flight simulation</h2>
<p class="whatif-lead">See exactly what a transaction will do <em>before</em> you send it —
run it against live state, get the failure explained in plain English, and see every
account it would change.</p>
<div class="tabs">
<button class="tab on" data-tab="build">Build a transaction</button>
<button class="tab" data-tab="paste">Paste base64 (advanced)</button>
</div>
<!-- paste mode -->
<div id="tabPaste" class="tabpane hidden">
<label class="flabel">Serialized transaction (base64)</label>
<textarea id="txB64" class="ta" spellcheck="false"
placeholder="paste the base64 of an unsigned VersionedTransaction — e.g. Buffer.from(tx.serialize()).toString('base64')"></textarea>
</div>
<!-- build mode -->
<div id="tabBuild" class="tabpane">
<label class="flabel">Program ID — we'll read its instructions from the on-chain IDL</label>
<div class="row-inline">
<input id="buildProg" class="finput mono" spellcheck="false" placeholder="program address…" />
<button class="btn ghost sm" id="loadIx">Load instructions</button>
</div>
<div id="ixHint" class="buildhint">
Pick any Anchor program and we'll list its instructions — fill in the accounts
(PDAs derive themselves) and simulate, no serialized transaction needed.
<div class="tryrow">Try:
<button class="trychip" data-prog="HqJiDmNcC1MBD5FTCQTddFX2zJTGW4XcCt8tZWTP7K9V" data-cluster="devnet"
data-payer="A1iQJhg25EPc8VwXXngJ58GwVJAsCzsMnt2ybSu93yvD" data-ix="claim">
vesting · claim <span class="dim">(devnet — fails now, works in +30 days)</span>
</button>
</div>
</div>
<div id="idlFallback" class="hidden" style="margin-top:14px">
<label class="flabel">No on-chain IDL — paste the program's Anchor IDL JSON</label>
<textarea id="idlJson" class="ta" spellcheck="false"
placeholder="paste the IDL JSON — e.g. the contents of target/idl/<program>.json"></textarea>
<button class="btn ghost sm" id="useIdl" style="margin-top:8px">Load instructions from IDL</button>
</div>
<div id="ixPicker" class="hidden">
<label class="flabel" style="margin-top:14px">Instruction</label>
<select id="ixSelect" class="finput"></select>
<div id="ixForm"></div>
</div>
</div>
<div class="editor-actions">
<button class="btn ghost sm" id="closeSim">← Back</button>
<span class="spacer"></span>
<button class="btn" id="runSim">Simulate ⟶</button>
</div>
<div id="simOut"></div>
</div>
</div>
<div id="out" class="hidden">
<div id="overview"></div>
<div class="grid">
<div class="card"><h2>Instruction call tree <span id="treeCount" class="count"></span></h2><div id="tree" class="tree"></div></div>
<div class="card"><h2>Compute units <span id="cuCount" class="count"></span></h2><div id="compute" class="rows"></div></div>
<div class="card"><h2>SOL balance changes <span id="diffCount" class="count"></span></h2><div id="diffs" class="rows"></div></div>
<div class="card"><h2>Token balance changes <span id="tokCount" class="count"></span></h2><div id="tokens" class="rows"></div></div>
</div>
<!-- The logs the transaction actually produced on-chain, shown immediately. -->
<div class="card full" style="margin-top:16px" id="onchainLogsCard">
<h2>Program logs <span id="logCount" class="count"></span></h2>
<div id="onchainLogs"></div>
</div>
<div class="card full" style="margin-top:16px"><h2>Local replay</h2><div id="replay"></div></div>
<div class="card full" style="margin-top:16px">
<h2>Scenario tests <span id="suiteSummary" class="count"></span></h2>
<p class="whatif-lead">svmscope replays the <em>real</em> programs against edited state, so you can assert what
<em>should</em> happen in each edge case — with no test harness to write. It starts you with a
suite auto-generated from this transaction; tweak the expectations, run them, and export the
suite to run in CI with <code>svmscope test suite.json</code>.</p>
<div class="suite-toolbar">
<button class="btn" id="runSuite">▶ Run all tests</button>
<span class="spacer"></span>
<span id="suiteResult" class="suite-result"></span>
<button class="btn ghost sm" id="freezeFixture" title="capture a self-contained snapshot so the suite runs deterministically offline">❄ Freeze fixture</button>
<button class="btn ghost sm" id="exportSuite">⤓ Export suite</button>
</div>
<div id="suite" class="suite"></div>
<div id="exportBox" class="export-box hidden"></div>
<details class="advanced">
<summary>Advanced — edit account fields & add a custom scenario <span id="acctCount" class="count"></span></summary>
<div class="adv-body">
<p class="whatif-lead" style="margin-bottom:12px">Edit any field below, then <b>Stage</b> your changes and
<b>Add as test scenario</b> to drop them into the suite above — or just <b>Simulate</b> once to preview.</p>
<div class="editor-toolbar">
<input id="acctFilter" class="filter" placeholder="filter accounts by name, address or type…" spellcheck="false" />
<span id="acctSummary" class="summary"></span>
<button class="btn ghost sm" id="expandAll">Expand all</button>
</div>
<div id="acctList"></div>
<div class="editor-actions">
<button class="btn ghost sm" id="stageBtn">+ Stage all changes</button>
<span class="spacer"></span>
<span id="dirtyCount" class="dirty-count"></span>
<button class="btn ghost sm" id="addScenario">+ Add as test scenario</button>
<button class="btn" id="simBtn">Simulate ⟶</button>
</div>
<div id="stagedWrap" class="hidden">
<div style="font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--muted);margin:18px 0 2px;font-weight:600">Staged mutations</div>
<div id="staged"></div>
</div>
<div id="simResult" class="sim-result"></div>
</div>
</details>
</div>
</div>
</main>
<footer class="foot">
<span><b style="color:var(--text-2)">svmscope</b> — decode · replay · mutate</span>
<span class="spacer"></span>
<a href="https://github.com/alizeeshan1234/svmScope" target="_blank" rel="noopener">GitHub</a>
<span class="sep">·</span>
<a href="/api" target="_blank" rel="noopener">API</a>
<span class="sep">·</span>
<span>runs on <a href="https://github.com/LiteSVM/litesvm" target="_blank" rel="noopener">LiteSVM</a></span>
</footer>
<!-- Optional: sets window.SVMSCOPE_API when the frontend is hosted apart from the
engine (Vercel). Absent when the Rust server serves this page itself. -->
<script src="config.js" onerror="void 0"></script>
<script>
const $ = (id) => document.getElementById(id);
const esc = (s) => String(s).replace(/[&<>"]/g, c => ({'&':'&','<':'<','>':'>','"':'"'}[c]));
const short = (a) => a && a.length > 12 ? a.slice(0, 4) + '…' + a.slice(-4) : (a || '');
const fmt = (n) => Number(n).toLocaleString('en-US');
// Unix seconds → "Aug 25, 2026 · 16:20 UTC" (matches how explorers show block time).
function fmtUtc(sec) {
const d = new Date(sec * 1000);
const s = d.toLocaleString('en-US', {
month: 'short', day: 'numeric', year: 'numeric',
hour: '2-digit', minute: '2-digit', hour12: false, timeZone: 'UTC',
});
// "Aug 25, 2026, 16:20" → "Aug 25, 2026 · 16:20 UTC" (split date from time only).
const i = s.lastIndexOf(', ');
return (i < 0 ? s : s.slice(0, i) + ' · ' + s.slice(i + 2)) + ' UTC';
}
let analysis = null, currentSig = '', mutations = [], mutableAccounts = [];
// API base. Empty = same origin (the Rust server serving this page locally).
// When the frontend is hosted separately (e.g. Vercel), window.SVMSCOPE_API is
// injected by config.js and points at the engine; ?api=<url> overrides for testing.
const API = (new URLSearchParams(location.search).get('api')
|| window.SVMSCOPE_API
|| '').replace(/\/$/, '');
const api = (path, init) => fetch(API + path, init);
// Prewarm the engine on page load. The free host sleeps after ~15 min idle; kicking
// off a cheap /api ping the instant someone lands means the container is already
// waking while they read the hero, so it's ready by the time they paste a signature.
// The same ping tells us whether this instance honors a caller-supplied RPC. Public
// instances disable it (SSRF safety), so hide the "Custom RPC…" option there rather
// than offer a control the server will ignore.
try {
fetch(API + '/api', { mode: 'cors', cache: 'no-store' })
.then(r => r.ok ? r.json() : null)
.then(info => {
if (info && info.custom_rpc === false) {
const opt = document.querySelector('#cluster option[value="custom"]');
if (opt) opt.remove();
}
})
.catch(() => {});
} catch {}
// Selected cluster — appended to GET requests and merged into POST bodies so one
// instance serves mainnet / devnet / testnet / localnet / a custom RPC endpoint.
const currentCluster = () => $('cluster').value;
// Request params for the current selection: a named cluster, or {rpc:<url>} when
// "Custom RPC…" is chosen (the backend prefers an explicit rpc over a cluster).
function clusterParams() {
if ($('cluster').value === 'custom') {
const url = ($('customRpc').value || '').trim();
return url ? { rpc: url } : {}; // empty custom → backend default (mainnet)
}
return { cluster: $('cluster').value };
}
const clusterQ = () => {
const q = new URLSearchParams(clusterParams()).toString();
return q ? '?' + q : '';
};
// ---------------- known-program & known-mint registries ----------------
const PROGRAMS = {
'11111111111111111111111111111111': ['System Program', '#8b93a7'],
'ComputeBudget111111111111111111111111111111': ['Compute Budget', '#8b93a7'],
'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA': ['Token Program', '#35d07f'],
'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb': ['Token-2022', '#35d07f'],
'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL': ['Associated Token', '#19d3f3'],
'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr': ['Memo Program', '#8b93a7'],
'Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo': ['Memo Program (v1)', '#8b93a7'],
'AddressLookupTab1e1111111111111111111111111': ['Address Lookup Table', '#8b93a7'],
'JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4': ['Jupiter Aggregator v6', '#ff9c3b'],
'JUP4Fb2cqiRUcaTHdrPC8h2gNsA2ETXiPDD33WcGuJB': ['Jupiter Aggregator v4', '#ff9c3b'],
'675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8': ['Raydium AMM v4', '#3b82f6'],
'CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK': ['Raydium CLMM', '#3b82f6'],
'CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C': ['Raydium CPMM', '#3b82f6'],
'whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc': ['Orca Whirlpools', '#60a5fa'],
'pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA': ['Pump AMM', '#9945ff'],
'6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P': ['Pump.fun', '#9945ff'],
'LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo': ['Meteora DLMM', '#f472b6'],
'Eo7WjKq67rjJQSZxS6z3YkapzY3eMj6Xy8X5EQVn5UaB': ['Meteora Pools', '#f472b6'],
'PhoeNiXZ8ByJGLkxNfZRnkUfjvmuYqLR89jjFHGqdXY': ['Phoenix', '#a78bfa'],
'srmqPvymJeFKQ4zGQed1GFppgkRHL9kaELCbyksJtPX': ['OpenBook / Serum', '#e879f9'],
'opnb2LAfJYbRMAHHvqjCwQxanZn7ReEHp1k81EohpZb': ['OpenBook v2', '#e879f9'],
'dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcozatx': ['Drift v2', '#22d3ee'],
};
const MINTS = {
'So11111111111111111111111111111111111111112': ['SOL', 9],
'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v': ['USDC', 6],
'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB': ['USDT', 6],
'mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So': ['mSOL', 9],
'7dHbWXmci3dT8UFYWYZweBLXgycu7Y3iL6trKn1Y7ARj': ['stSOL', 9],
'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263': ['BONK', 5],
'JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN': ['JUP', 6],
};
// deterministic pastel hue for an unknown program, from its address
function hashHue(s) { let h = 0; for (const c of s) h = (h * 31 + c.charCodeAt(0)) & 0xffff; return h % 360; }
function progInfo(id) {
if (PROGRAMS[id]) return { name: PROGRAMS[id][0], color: PROGRAMS[id][1], known: true };
return { name: short(id), color: `hsl(${hashHue(id)} 45% 62%)`, known: false };
}
function mintInfo(mint) {
if (MINTS[mint]) return { sym: MINTS[mint][0], dec: MINTS[mint][1], known: true };
return { sym: short(mint), dec: null, known: false };
}
// ---------------- base58 (pubkey editing) ----------------
const B58 = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
function bs58decode(str) {
const map = {}; for (let i = 0; i < B58.length; i++) map[B58[i]] = i;
const bytes = [0];
for (const ch of str.trim()) {
if (!(ch in map)) throw new Error('invalid base58 character: ' + ch);
let carry = map[ch];
for (let j = 0; j < bytes.length; j++) { carry += bytes[j] * 58; bytes[j] = carry & 0xff; carry >>= 8; }
while (carry) { bytes.push(carry & 0xff); carry >>= 8; }
}
for (const ch of str.trim()) { if (ch === '1') bytes.push(0); else break; }
return bytes.reverse();
}
const toHex = (arr) => arr.map(b => b.toString(16).padStart(2, '0')).join('');
function encodeField(field, val) {
val = val.trim();
switch (field.type) {
case 'u64': {
if (!/^\d+$/.test(val)) throw new Error(field.name + ': must be a whole number');
const v = BigInt(val); if (v > 0xffffffffffffffffn) throw new Error(field.name + ': out of u64 range');
let hex = '', x = v; for (let i = 0; i < 8; i++) { hex += (x & 0xffn).toString(16).padStart(2, '0'); x >>= 8n; } return hex; }
case 'u8': { if (!/^\d+$/.test(val)) throw new Error(field.name + ': must be a whole number'); const v = Number(val); if (v > 255) throw new Error(field.name + ': must be 0–255'); return v.toString(16).padStart(2, '0'); }
case 'bool': { const t = /^(1|true|yes)$/i.test(val), f = /^(0|false|no)$/i.test(val); if (!t && !f) throw new Error(field.name + ': must be true/false'); return t ? '01' : '00'; }
case 'pubkey': { const bytes = bs58decode(val); if (bytes.length !== 32) throw new Error(field.name + ': not a 32-byte pubkey'); return toHex(bytes); }
default: throw new Error(field.name + ': not editable');
}
}
// ---------------- formatting helpers ----------------
function fmtSol(lamports) {
const s = (Number(lamports) / 1e9);
const str = s.toLocaleString('en-US', { maximumFractionDigits: 9 });
return str + ' SOL';
}
// raw base units (string) → human amount using decimals
function fmtToken(rawStr, decimals) {
let raw = BigInt(rawStr); const neg = raw < 0n; if (neg) raw = -raw;
const base = 10n ** BigInt(decimals);
const whole = raw / base, frac = raw % base;
let out = whole.toLocaleString('en-US');
if (decimals > 0) {
let f = frac.toString().padStart(decimals, '0').replace(/0+$/, '');
if (f) out += '.' + f;
}
return (neg ? '-' : '') + out;
}
function copyPill(addr, text) {
return `<span class="addr-pill" data-copy="${esc(addr)}" title="${esc(addr)} — click to copy">${esc(text || short(addr))}</span>`;
}
// program chip: colored dot + name (+ faded address when named)
function progChip(id) {
const p = progInfo(id);
const dot = `<span class="dot" style="color:${p.color};background:${p.color}"></span>`;
if (p.known) return `<span class="prog">${dot}<span class="nm">${esc(p.name)}</span>${copyPill(id, short(id))}</span>`;
return `<span class="prog">${dot}<span class="nm">${copyPill(id, short(id))}</span></span>`;
}
function progPill(id) {
const p = progInfo(id);
return `<span class="pill"><span class="dot" style="color:${p.color};background:${p.color}"></span>` +
`<span>${esc(p.name)}</span>${p.known ? `<span class="ad">${esc(short(id))}</span>` : ''}</span>`;
}
// ---------------- section renderers ----------------
// The "headline" program: the top-level instruction program that burned the most
// compute (the real work), ignoring infra programs. Falls back to the last one.
function primaryProgram(top, compute) {
const cu = {}; (compute || []).forEach(r => { cu[r.program] = r.cu; });
const infra = new Set(Object.keys(PROGRAMS).filter(k => /System|Compute|Associated|Memo|Lookup|Token Program|Token-2022/.test(PROGRAMS[k][0])));
const app = top.filter(id => !infra.has(id));
const pool = app.length ? app : top;
return pool.slice().sort((a, b) => (cu[b] || 0) - (cu[a] || 0))[0] || null;
}
function renderOverview(o, compute) {
const ok = o.success;
const prim = primaryProgram(o.top_programs, compute);
const title = prim ? progInfo(prim).name : 'Transaction';
const stats = [
['Fee', fmtSol(o.fee)],
['Compute', (o.compute_units != null ? fmt(o.compute_units) : '—') + ' CU'],
['Instructions', o.top_programs.length],
];
if (o.slot != null) stats.push(['Slot', fmt(o.slot)]);
const progs = o.top_programs.map(progPill).join('');
// A secondary detail strip — the metadata a block explorer surfaces.
const meta = [];
if (o.fee_payer) meta.push(['Fee payer', copyPill(o.fee_payer, short(o.fee_payer))]);
if (o.version) meta.push(['Version', esc(o.version)]);
if (o.account_count) meta.push(['Accounts', fmt(o.account_count)]);
if (o.block_time != null) meta.push(['Timestamp', esc(fmtUtc(o.block_time))]);
if (o.recent_blockhash) meta.push(['Blockhash', copyPill(o.recent_blockhash, short(o.recent_blockhash))]);
const metaRow = meta.length
? `<div class="ov-meta">${meta.map(([l, v]) => `<div class="ov-m"><span class="l">${l}</span><span class="v">${v}</span></div>`).join('')}</div>`
: '';
$('overview').innerHTML = `
<div class="overview">
<div class="ov-top">
<div class="ov-status">
<div class="ov-ring ${ok ? 'ok' : 'fail'}">${ok ? '✓' : '✕'}</div>
<div>
<div class="ov-title">${esc(title)}</div>
<div class="ov-sub">${ok ? 'Succeeded on-chain' : 'Failed on-chain'}</div>
</div>
</div>
<div class="ov-spacer"></div>
<div class="ov-stats">
${stats.map(([l, v]) => `<div class="ov-stat"><div class="l">${l}</div><div class="v">${esc(v)}</div></div>`).join('')}
</div>
</div>
${metaRow}
<div class="ov-progs">${progs}</div>
</div>`;
wireCopy($('overview'));
}
function renderTree(entries) {
$('treeCount').textContent = entries.length;
$('tree').innerHTML = entries.map((e, i) => {
const depth = Math.max(1, e.stack_height);
const rail = depth > 1 ? '│ '.repeat(depth - 2) + '└── ' : '';
const ix = depth === 1 ? `<span class="ix">#${e.index}</span>` : `<span class="ix"></span>`;
// The decoded instruction name — "Route V2", "Swap V2", "Transfer" — is what
// turns a wall of program ids into a readable call trace.
const nm = e.name ? `<span class="ixname">${esc(e.name)}</span>` : '';
const hasDetail = (e.args && e.args.length) || (e.accounts && e.accounts.length);
const caret = hasDetail ? `<span class="ix-caret">▸</span>` : '';
const head = `<div class="tnode${hasDetail ? ' expandable' : ''}"${hasDetail ? ` data-ix="${i}"` : ''}>` +
`${ix}<span class="rail">${rail}</span>${caret}${progChip(e.program)}${nm}` +
(depth > 1 ? `<span class="depth">depth ${depth}</span>` : '') + `</div>`;
return head + (hasDetail ? renderIxDetail(e, i) : '');
}).join('');
// Toggle a detail panel on click.
$('tree').querySelectorAll('.tnode.expandable').forEach(n => n.addEventListener('click', ev => {
if (ev.target.closest('.addr-pill')) return; // let copy-clicks through
const panel = document.getElementById('ixd-' + n.dataset.ix);
if (panel) { panel.classList.toggle('open'); n.classList.toggle('on'); }
}));
wireCopy($('tree'));
}
// The developer payload: an instruction's decoded arguments and named accounts,
// shown on demand under its row.
function renderIxDetail(e, i) {
const args = (e.args || []).map(a => {
// Format token/lamport amounts as raw (the decoded value) with the type tag.
const val = a.value === '' ? '<span class="ix-var">variable-length →</span>' : esc(a.value);
return `<div class="ix-row"><span class="ix-k">${esc(a.name)} <span class="ix-ty">${esc(a.type)}</span></span><span class="ix-v">${val}</span></div>`;
}).join('');
const accts = (e.accounts || []).map(ac => {
const nm = ac.name ? `<span class="ix-an">${esc(ac.name)}</span>` : `<span class="ix-an dim">account</span>`;
return `<div class="ix-row">${nm}<span class="ix-v">${copyPill(ac.address, short(ac.address))}</span></div>`;
}).join('');
return `<div class="ixdetail" id="ixd-${i}">` +
(args ? `<div class="ixd-sec"><div class="ixd-h">Arguments</div>${args}</div>` : '') +
(accts ? `<div class="ixd-sec"><div class="ixd-h">Accounts <span class="ixd-n">${e.accounts.length}</span></div>${accts}</div>` : '') +
`</div>`;
}
function renderCompute(rows) {
$('cuCount').textContent = rows.length;
if (!rows.length) { $('compute').innerHTML = '<div class="empty">no compute recorded</div>'; return; }
const max = Math.max(...rows.map(r => r.cu), 1);
$('compute').innerHTML = rows.map(r => `
<div class="row">
<div class="grow">${progChip(r.program)}<div class="cu-bar" style="width:${Math.max(3, r.cu / max * 100)}%"></div></div>
<div class="cu-val">${fmt(r.cu)} <small>CU</small></div>
</div>`).join('');
}
function renderDiffs(changes) {
$('diffCount').textContent = changes.length;
if (!changes.length) { $('diffs').innerHTML = '<div class="empty">no SOL balance changes</div>'; return; }
$('diffs').innerHTML = changes.map(c => {
const cls = c.delta >= 0 ? 'pos' : 'neg', sign = c.delta >= 0 ? '+' : '−';
const post = c.post != null ? `<div class="post-bal">${fmtSol(c.post)}</div>` : '';
return `<div class="row"><div class="grow">${copyPill(c.address)}</div>` +
`<div class="amt-col"><div class="amt ${cls}">${sign}${fmtSol(Math.abs(c.delta))}</div>${post}</div></div>`;
}).join('');
}
function renderTokens(changes) {
$('tokCount').textContent = changes.length;
if (!changes.length) { $('tokens').innerHTML = '<div class="empty">no token balance changes</div>'; return; }
$('tokens').innerHTML = changes.map(c => {
const m = mintInfo(c.mint);
const dec = m.dec != null ? m.dec : c.decimals;
const neg = c.delta_raw.startsWith('-');
const cls = neg ? 'neg' : 'pos', sign = neg ? '' : '+';
return `<div class="row">
<div class="grow">
<div><span class="tok-sym">${esc(m.sym)}</span> <span class="sub-owner">owner ${copyPill(c.owner)}</span></div>
<div class="tok-meta">${copyPill(c.address)} · ${m.known ? esc(m.sym) + ' mint' : 'mint ' + esc(short(c.mint))}</div>
</div>
<div class="amt ${cls}">${sign}${esc(fmtToken(c.delta_raw, dec))}</div>
</div>`;
}).join('');
}
// Render program logs like a real trace viewer: indent by CPI depth, name the
// programs, highlight instructions, and dim the boilerplate.
function renderLogs(logs) {
let depth = 0;
const rows = logs.map((l) => {
const invoke = l.match(/^Program (\S+) invoke \[(\d+)\]/);
let indent, html;
if (invoke) {
const pid = invoke[1], lvl = Number(invoke[2]);
indent = lvl - 1; depth = lvl;
const p = progInfo(pid);
html = `<span class="lg-dot" style="color:${p.color};background:${p.color}"></span>` +
`<span class="lg-prog" style="color:${p.color}">${esc(p.known ? p.name : short(pid))}</span>` +
`<span class="lg-kw">invoke</span><span class="lg-lvl">#${lvl}</span>`;
} else if (/^Program \S+ success$/.test(l)) {
depth = Math.max(0, depth - 1); indent = depth;
const pid = l.split(' ')[1], p = progInfo(pid);
html = `<span class="lg-prog" style="color:${p.color}">${esc(p.known ? p.name : short(pid))}</span> <span class="lg-ok">✓ success</span>`;
} else if (/failed|AnchorError|Error (Code|Number|Message)|panicked/i.test(l)) {
indent = depth;
html = `<span class="lg-fail">${esc(l.replace(/^Program \S+ /, ''))}</span>`;
} else if (/consumed \d+ of \d+ compute units/.test(l)) {
indent = depth;
const m = l.match(/consumed (\d+) of (\d+)/);
html = `<span class="lg-dim">consumed ${fmt(m[1])} of ${fmt(m[2])} CU</span>`;
} else if (/^Program log: Instruction: /.test(l)) {
indent = depth;
html = `<span class="lg-kw">ix</span><span class="lg-inst">${esc(l.replace('Program log: Instruction: ', ''))}</span>`;
} else if (/^Program (log|data|return):/.test(l)) {
indent = depth;
const kw = l.match(/^Program (log|data|return):/)[0];
const body = l.slice(kw.length).trim();
html = `<span class="lg-key">${esc(kw)}</span> <span class="lg-msg">${esc(body)}</span>`;
} else {
indent = depth;
html = `<span class="lg-dim">${esc(l)}</span>`;
}
return `<div class="lg-line" style="padding-left:${indent * 18}px">${html}</div>`;
});
return rows.join('');
}
// Local replay is opt-in. Show a button until the user runs it (or it's been
// run once as a scenario baseline).
// The transaction's real on-chain logs — available straight from metadata, so
// they show without waiting for a local replay (this is what an explorer shows).
function renderOnchainLogs(logs) {
const card = $('onchainLogsCard');
if (!logs || !logs.length) { card.classList.add('hidden'); return; }
card.classList.remove('hidden');
$('logCount').textContent = logs.length + ' lines';
$('onchainLogs').innerHTML = `<div class="logs">${renderLogs(logs)}</div>`;
}
function renderReplayPanel() {
const warped = !!Object.keys(timeTravel).length;
const feated = !!activeFeatures.length;
// With the clock shifted or feature gates flipped, replay through /replay_report
// so we run under that environment and get the account diff; otherwise the plain
// cached replay is enough.
if (warped || feated) {
$('replay').innerHTML = '<div class="status" style="text-align:left"><span class="spinner"></span>replaying under the selected environment…</div>';
api('/replay_report', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ signature: currentSig, mutations: [], time_travel: timeTravel, features: activeFeatures, ...clusterParams() }),
})
.then(async r => { if (!r.ok) throw new Error(await r.text()); return r.json(); })
.then(rep => renderReport(rep, $('replay')))
.catch(e => { $('replay').innerHTML = `<div class="err-box">${esc(e.message)}</div>`; });
return;
}
const body = analysis.replay
? renderReplay(analysis.replay)
: `<button class="btn ghost sm" id="runReplay">▶ Run local replay</button>
<span class="cu-note" style="margin-left:10px">Re-execute this transaction locally against current state
(it succeeded on-chain; local replay can differ if state has since moved).</span>`;
$('replay').innerHTML = body;
$('runReplay')?.addEventListener('click', async () => {
const b = $('runReplay'); b.disabled = true; b.textContent = '▶ Replaying…';
try { await ensureBaseline(); renderReplayPanel(); }
catch (e) { $('replay').innerHTML = `<div class="err-box">${esc(e.message)}</div>`; }
});
}
// Fetch the baseline replay once and cache it on `analysis.replay`.
async function ensureBaseline() {
if (analysis.replay) return analysis.replay;
const res = await api('/replay/' + encodeURIComponent(currentSig) + clusterQ());
if (!res.ok) throw new Error(await res.text());
analysis.replay = await res.json();
return analysis.replay;
}
// Recognise the classic state-drift failure signatures, so a developer sees *why*
// a replay reverted without having to know each program's error codes.
function driftHint(r) {
if (r.success) return '';
// Include the IDL-resolved error name (e.g. "SlippageToleranceExceeded") so a
// bare Custom(6001) is still recognised as slippage.
const blob = (r.error || '') + ' ' + (r.error_name || '') + ' ' + (r.logs || []).join(' ');
const table = [
[/AccountOwnedByWrongProgram|owned by a different program/i, 'an account this transaction needs has since been closed or reallocated, so it is now owned by a different program'],
[/AccountNotFound|no longer exists|InsufficientFunds|insufficient funds|insufficient lamports/i, 'an account the transaction needs no longer holds the state (or lamports) it did when this landed'],
[/InvalidTimestamp|Timestamp should be/i, 'an oracle / clock check — the account state is newer than the transaction'],
[/slippage|SlippageToleranceExceeded|ExceededSlippage/i, 'the pool price has moved since this landed, so the swap slips (common on 0-slippage swaps)'],
];
for (const [re, why] of table) if (re.test(blob)) return why;
return '';
}
function renderReplay(r) {
const badge = r.success ? `<span class="badge ok"><span class="b-dot"></span>replay success</span>`
: `<span class="badge fail"><span class="b-dot"></span>replay failed</span>`;
const hint = driftHint(r);
const note = hint
? `<div class="drift-inline">This looks like <b>state drift</b>, not a bug: ${hint}. It succeeded on-chain — replaying against <i>current</i> state can differ. <b>❄ Freeze fixture</b> to pin the state, or try a recent / simpler transaction.</div>`
: '';
// Lead the error box with the resolved name where we have one.
const errText = r.error ? (r.error_name ? `${r.error_name} — ${r.error}` : r.error) : '';
const err = errText ? `<div class="err-box">${esc(errText)}</div>` : '';
const logs = r.logs && r.logs.length ? `<div class="logs">${renderLogs(r.logs)}</div>` : '';
return `<div class="replay-head">${badge}<span class="cu-note"><b>${fmt(r.compute_units)}</b> compute units</span></div>${note}${err}${logs}`;
}
// ---------------- one-click what-if scenarios ----------------
function u64ToLeHex(bi) { let x = bi & 0xffffffffffffffffn, h = ''; for (let i = 0; i < 8; i++) { h += (x & 0xffn).toString(16).padStart(2, '0'); x >>= 8n; } return h; }
let suiteRows = [], rowSeq = 0, suiteDrift = '';
const rowById = (id) => suiteRows.find(r => String(r.id) === String(id));
const makeRow = (name, muts, expect, changeText, contains = '') =>
({ id: ++rowSeq, name, muts, expect, changeText, contains, status: null, got: '', actual: null, _open: false });
// Seed a starter test suite from the transaction's own accounts — the edge cases
// a developer of this kind of program would want to have covered.
function buildSuite() {
suiteRows = [];
suiteDrift = '';
suiteRows.push(makeRow('Baseline — transaction unchanged', [], analysis.overview.success ? 'success' : 'any', 'no changes'));
const toks = mutableAccounts
.filter(a => a.decoded && a.decoded.type_name === 'SPL Token Account')
.map(a => {
const amt = BigInt(a.decoded.fields.find(f => f.name === 'amount').value);
const mint = a.decoded.fields.find(f => f.name === 'mint').value;
const mi = mintInfo(mint);
return { a, amt, dec: mi.dec ?? 0, known: mi.known, sym: mi.sym };
})
.filter(x => x.amt > 0n)
.sort((p, q) => (q.amt > p.amt ? 1 : q.amt < p.amt ? -1 : 0));
const nm = (t) => t.known ? `${t.sym} account ${short(t.a.address)}` : `token account ${short(t.a.address)}`;
// The tx's own token flows decide the expectation: emptying an account the tx
// *draws from* must revert; an account that only receives shrugs it off.
const deltaOf = (addr) => {
const c = (analysis.token_change || []).find(ch => ch.address === addr);
return c ? BigInt(c.delta_raw) : null;
};
const isSource = (t) => { const d = deltaOf(t.a.address); return d !== null && d < 0n; };
// Prefer source accounts for the "empty it" scenarios — they make the point.
const emptyTargets = toks.slice().sort((p, q) =>
(isSource(q) - isSource(p)) || (q.amt > p.amt ? 1 : q.amt < p.amt ? -1 : 0));
emptyTargets.slice(0, 2).forEach(t => suiteRows.push(makeRow(
`Empty ${nm(t)}`, [{ kind: 'data', address: t.a.address, offset: 64, bytes_hex: '0000000000000000' }],
isSource(t) ? 'revert' : 'success',
`token amount ${fmtToken(t.amt.toString(), t.dec)} → 0` + (isSource(t) ? '' : ' (receive-only — should be harmless)'))));
if (toks.length) {
// Freeze an account the tx actually moves tokens through — the token program
// rejects transfers both into and out of a frozen account.
const moved = toks.find(t => { const d = deltaOf(t.a.address); return d !== null && d !== 0n; }) || toks[0];
suiteRows.push(makeRow(`Freeze ${nm(moved)}`,
[{ kind: 'data', address: moved.a.address, offset: 108, bytes_hex: '02' }], 'revert', `account state → 2 (frozen)`));
// Doubling a pool vault shifts the quote (slippage!), so only double a token
// account the fee payer owns — extra balance in the user's wallet is harmless.
const ownerOf = (t) => (t.a.decoded.fields.find(f => f.name === 'owner') || {}).value;
const userTok = toks.find(t => ownerOf(t) === analysis.overview.fee_payer);
if (userTok) suiteRows.push(makeRow(`Double ${nm(userTok)}`,
[{ kind: 'data', address: userTok.a.address, offset: 64, bytes_hex: u64ToLeHex(userTok.amt * 2n) }], 'success', `token amount → 2×`));
}
// Drain the fee payer — it always has to cover the fee, so this must revert.
// Fall back to the largest system-owned wallet if the payer isn't mutable here.
const wallets = mutableAccounts.filter(a => a.owner === SYSTEM_PROGRAM).sort((p, q) => q.lamports - p.lamports);
const payer = wallets.find(w => w.address === analysis.overview.fee_payer);
const drain = payer || wallets[0];
if (drain && drain.lamports > 0) {
suiteRows.push(makeRow(payer ? 'Drain the fee payer wallet' : 'Drain the largest SOL wallet',
[{ kind: 'lamports', address: drain.address, lamports: 0 }], 'revert', `${short(drain.address)} → 0 SOL`));
}
}
function renderSuite() {
if (!suiteRows.length) { $('suite').innerHTML = '<div class="empty">No scenarios yet — add one from the advanced editor below.</div>'; $('suiteSummary').textContent = ''; return; }
$('suiteSummary').textContent = suiteRows.length + ' scenarios';
$('suite').innerHTML = suiteDrift + suiteRows.map(r => {
const st = r.status === 'pass' ? '✅' : r.status === 'fail' ? '❌' : '<span class="pend">•</span>';
const contains = r.expect === 'revert'
? `<input class="contains" data-id="${r.id}" placeholder="error has…" value="${esc(r.contains)}" spellcheck="false" title="optional: require the error/logs to contain this text" />` : '';
const logs = (r.actual && r._open) ? `<div class="srow-logs">${renderReplaySmall(r.actual)}</div>` : '';
return `<div class="srow ${r.status || ''}" data-id="${r.id}">
<div class="srow-head">
<div class="st">${st}</div>
<div class="info"><div class="nm">${esc(r.name)}</div><div class="ch">${esc(r.changeText)}${r.actual ? ' · click to see logs' : ''}</div></div>
<div class="expect-sel"><label>expect</label>
<select class="exp" data-id="${r.id}">
<option value="success"${r.expect === 'success' ? ' selected' : ''}>succeeds</option>
<option value="revert"${r.expect === 'revert' ? ' selected' : ''}>reverts</option>
<option value="any"${r.expect === 'any' ? ' selected' : ''}>any</option>
</select>${contains}</div>
<div class="got">${esc(r.got || '')}</div>
<button class="rm" data-id="${r.id}" title="remove">✕</button>
</div>${logs}</div>`;
}).join('');
$('suite').querySelectorAll('select.exp').forEach(s => s.addEventListener('change', () => { const r = rowById(s.dataset.id); r.expect = s.value; r.status = null; r.got = ''; renderSuite(); }));
$('suite').querySelectorAll('input.contains').forEach(i => i.addEventListener('input', () => { rowById(i.dataset.id).contains = i.value; }));
$('suite').querySelectorAll('.rm').forEach(b => b.addEventListener('click', () => { suiteRows = suiteRows.filter(r => String(r.id) !== b.dataset.id); renderSuite(); }));
$('suite').querySelectorAll('.srow .info').forEach(el => el.addEventListener('click', () => { const r = rowById(el.closest('.srow').dataset.id); if (r.actual) { r._open = !r._open; renderSuite(); } }));
}
function renderReplaySmall(r) {
const err = r.error ? `<div class="err-box" style="margin-top:0">${esc(r.error)}</div>` : '';
const logs = r.logs && r.logs.length ? `<div class="logs" style="margin-top:8px;max-height:200px">${renderLogs(r.logs)}</div>` : '';
return err + logs || '<div class="empty">no logs</div>';
}
async function runSuite() {
if (!analysis || !suiteRows.length) return;
const btn = $('runSuite'); btn.disabled = true;
const el = $('suiteResult'); el.className = 'suite-result'; el.innerHTML = '<span class="spinner"></span>running…';
try {
const scenarios = suiteRows.map(r => {
const s = { name: r.name, expect: r.expect, mutations: r.muts };
if (r.expect === 'revert' && r.contains) s.contains = r.contains;
return s;
});
const res = await api('/simulate_suite', { method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ signature: currentSig, scenarios, time_travel: timeTravel, features: activeFeatures, ...clusterParams() }) });
if (!res.ok) throw new Error(await res.text());
const outcomes = await res.json();
outcomes.forEach((o, i) => {
const r = suiteRows[i]; r.status = o.pass ? 'pass' : 'fail'; r.actual = o.actual;
r.got = o.actual.success ? '✓ succeeded' : `✗ ${friendlyReason(o.actual)}`;
});
const passed = outcomes.filter(o => o.pass).length;
el.className = 'suite-result ' + (passed === outcomes.length ? 'pass' : 'fail');
el.textContent = `${passed} / ${outcomes.length} passed`;
// If the *unmodified* transaction reverts on replay, the whole suite is
// dominated by state drift, not by the mutations — say so plainly instead of
// showing a wall of unexplained red.
const baseIdx = suiteRows.findIndex(r => !(r.muts && r.muts.length));
const base = baseIdx >= 0 ? outcomes[baseIdx] : null;
suiteDrift = (base && base.actual && !base.actual.success)
? `<div class="drift-note">
<b>The unmodified transaction reverts on local replay</b> — its on-chain state has moved
since it landed (<code>${esc(friendlyReason(base.actual))}</code>). This is expected for
older or complex transactions like swaps, where current pool prices differ from the slot
it ran in. The what-if assertions below are only meaningful once the baseline replays
cleanly — <b>❄ Freeze fixture</b> to pin the state, or try a <b>recent / simpler</b> transaction.
</div>`
: '';
renderSuite();
} catch (e) { el.className = 'suite-result fail'; el.textContent = 'error: ' + e.message; }
finally { btn.disabled = false; }
}
async function freezeFixture() {
if (!currentSig) return;
const btn = $('freezeFixture'), old = btn.textContent;
btn.disabled = true; btn.textContent = '❄ Freezing…';
try {
const res = await api('/freeze/' + encodeURIComponent(currentSig) + clusterQ());
if (!res.ok) throw new Error(await res.text());
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = 'fixture.json';
document.body.appendChild(a); a.click(); a.remove();
setTimeout(() => URL.revokeObjectURL(url), 1000);
btn.textContent = '❄ Saved fixture.json ✓';
setTimeout(() => { btn.textContent = old; }, 2500);
} catch (e) {
btn.textContent = old;
$('suiteResult').className = 'suite-result fail';
$('suiteResult').textContent = 'freeze failed: ' + e.message;
} finally { btn.disabled = false; }
}
function exportSuite() {
if (!suiteRows.length) return;
// A fixture-referencing suite: hermetic and CI-safe once you freeze the fixture.
const spec = { fixture: 'fixture.json', scenarios: suiteRows.map(r => {
const o = { name: r.name, expect: r.expect };
if (r.expect === 'revert' && r.contains) o.contains = r.contains;
o.mutations = r.muts; return o;
}) };
const json = JSON.stringify(spec, null, 2);
const box = $('exportBox'); box.classList.remove('hidden');
box.innerHTML = `<pre>${esc(json)}</pre>
<div class="cmd">1. click <b>❄ Freeze fixture</b> to save <b>fixture.json</b><br>
2. save this as <b>suite.json</b> beside it — add <code>"asserts"</code> to check resulting state, by name:
<code>{"kind":"field","field":"amount","op":">=","value":1000}</code><br>
3. run in CI: <b>svmscope test suite.json</b> — deterministic, offline, exits non-zero on any failure</div>
<button class="btn ghost sm" id="copyExport" style="margin-top:10px">Copy JSON</button>`;
$('copyExport').addEventListener('click', async () => { try { await navigator.clipboard.writeText(json); } catch {} $('copyExport').textContent = 'Copied ✓'; });
box.scrollIntoView({ block: 'nearest' });
}
function addScenarioFromEditor() {
let muts;
try { muts = [...mutations, ...collectAllEdits()]; } catch (e) { flashSim(e.message, true); return; }
if (!muts.length) { flashSim('stage or edit a change first, then add it as a scenario', true); return; }
const label = muts.map(m => m._label).join(', ');
suiteRows.push(makeRow(`Custom — ${label}`, muts.map(({ _label, ...m }) => m), 'any', label));
mutations = [];
$('acctList').querySelectorAll('input.f-in.dirty').forEach(i => { i.value = i.dataset.cur; i.classList.remove('dirty'); });
updateDirtyState(); renderStaged(); renderSuite();
flashSim('added to the scenario suite above ✓');
}
// Translate a failure into a plain-English reason (best effort, from the logs).
function friendlyReason(after) {
for (const l of (after.logs || [])) {
let m = l.match(/Error Message:\s*(.+?)\.?$/); if (m) return m[1];
if (/insufficient funds/i.test(l)) return 'an account had insufficient funds';
if (/slippage/i.test(l)) return 'the price moved past the slippage limit';
}
if (after.error) {
if (/AccountNotFound/.test(after.error)) return 'an account no longer exists (0 lamports means it was deleted)';
if (/InsufficientFunds/i.test(after.error)) return 'an account had insufficient funds';
return after.error;
}
return 'the transaction was rejected';
}
// ---------------- what-if editor ----------------
function accountHint(a) {
if (a.decoded) {
if (a.decoded.type_name === 'SPL Token Account') {
const mintf = a.decoded.fields.find(f => f.name === 'mint');
const amtf = a.decoded.fields.find(f => f.name === 'amount');
const m = mintf ? mintInfo(mintf.value) : { sym: '', dec: 0 };
const dec = m.dec != null ? m.dec : 0;
return `${esc(m.sym)} · ${fmtToken(amtf.value, dec)}`;
}
if (a.decoded.type_name === 'SPL Mint') {
const sup = a.decoded.fields.find(f => f.name === 'supply');
const dc = a.decoded.fields.find(f => f.name === 'decimals');
const m = mintInfo(a.address);
return `${m.known ? esc(m.sym) + ' · ' : ''}supply ${fmtToken(sup.value, Number(dc.value))}`;
}
}
return fmtSol(a.lamports);
}
const SYSTEM_PROGRAM = '11111111111111111111111111111111';
const TOKEN_PROGRAM = 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA';
const TOKEN_2022 = 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb';
// A human name for an account, derived from its decoded type or owning program.
function accountName(a) {
if (a.decoded) {
const tn = a.decoded.type_name;
if (tn === 'SPL Mint') {
const m = mintInfo(a.address);
return m.known ? `${m.sym} Mint` : 'SPL Mint';
}
if (tn === 'SPL Token Account') {
const mintf = a.decoded.fields.find(f => f.name === 'mint');
const m = mintf ? mintInfo(mintf.value) : { known: false };
return m.known ? `${m.sym} Token Account` : 'Token Account';
}
return tn; // IDL-decoded account, e.g. "Pool", "LbPair", "FeeConfig"
}
if (a.owner === SYSTEM_PROGRAM) return 'Wallet';
if (PROGRAMS[a.owner]) return `${PROGRAMS[a.owner][0]} account`;
if (a.owner === TOKEN_PROGRAM || a.owner === TOKEN_2022) return 'Token account';
return 'Data account';
}
function accountTypeTag(a) {
const name = accountName(a);
if (a.decoded) {
const tn = a.decoded.type_name;
if (tn === 'SPL Mint') return `<span class="type-tag mint">${esc(name)}</span>`;
if (tn === 'SPL Token Account') return `<span class="type-tag">${esc(name)}</span>`;
// IDL-decoded: tint with the owning program's brand color when we know it.
if (PROGRAMS[a.owner]) {
const c = PROGRAMS[a.owner][1];
return `<span class="type-tag" style="color:${c};border-color:${c}55;background:${c}1e">${esc(name)}</span>`;
}
return `<span class="type-tag">${esc(name)}</span>`;
}
if (a.owner === SYSTEM_PROGRAM) return `<span class="type-tag sys">${esc(name)}</span>`;
if (PROGRAMS[a.owner]) {
const c = PROGRAMS[a.owner][1];
return `<span class="type-tag" style="color:${c};border-color:${c}55;background:${c}1e">${esc(name)}</span>`;
}
return `<span class="type-tag raw">${esc(name)}</span>`;
}
function buildEditorBody(a) {
let rows = `
<tr>
<td><span class="f-name">lamports</span></td>
<td><span class="f-type">u64</span></td>
<td class="f-cur">${fmt(a.lamports)}</td>
<td><input class="f-in" data-kind="lamports" data-cur="${a.lamports}" value="${a.lamports}" spellcheck="false" /></td>
</tr>`;
if (a.decoded) {
rows += a.decoded.fields.map((f, i) => {
const cur = esc(f.value);
const note = f.note ? `<div class="f-off">${esc(f.note)}</div>` : '';
const cell = f.editable
? `<input class="f-in" data-kind="field" data-fi="${i}" data-cur="${cur}" value="${cur}" spellcheck="false" />`
: `<span class="ro-dash" title="read-only (option/tag field)">${cur} <span style="color:var(--line-2)">· read-only</span></span>`;
return `<tr><td><span class="f-name">${esc(f.name)}</span> <span class="f-off">@${f.offset}</span>${note}</td>
<td><span class="f-type">${esc(f.type)}</span></td>
<td class="f-cur${f.editable ? '' : ' ro'}">${cur}</td><td>${cell}</td></tr>`;
}).join('');
}
let body = `<div class="acct-meta"><span>owner ${copyPill(a.owner)}</span><span>${fmt(a.lamports)} lamports · ${a.data_len} bytes</span></div>
<table class="fields"><thead><tr><th>Field</th><th>Type</th><th>Current</th><th>New value</th></tr></thead><tbody>${rows}</tbody></table>`;
if (!a.decoded) {
body += `<div class="raw-note">Unrecognized layout — no named fields. Patch raw bytes at an offset (hex, e.g. <code>00e1f505</code>).
<div class="raw-inputs"><input class="raw-off" type="number" placeholder="offset" style="max-width:110px" />
<input class="raw-hex" placeholder="hex bytes" style="flex:1" spellcheck="false" /></div></div>`;
}
return body;
}
function buildCard(a) {
const key = (a.address + ' ' + a.owner + ' ' + accountName(a) + ' ' + accountHint(a)).toLowerCase();
return `<div class="acct-card" data-address="${esc(a.address)}" data-key="${esc(key)}">
<button class="acct-head" type="button"><span class="chev">▶</span><span class="edit-dot hidden"></span>
${accountTypeTag(a)}${copyPill(a.address)}<span class="hint">${accountHint(a)}</span></button>
<div class="acct-body hidden">${buildEditorBody(a)}</div></div>`;
}
function renderAccountList() {
mutableAccounts = analysis.accounts.filter(a => !a.executable);
const n = mutableAccounts.length;
$('acctCount').textContent = n + ' mutable';
$('acctSummary').textContent = n + ' mutable account' + (n !== 1 ? 's' : '');
$('acctList').innerHTML = n ? mutableAccounts.map(buildCard).join('') : '<div class="empty" style="margin:12px 0">no mutable accounts</div>';
wireCards(); updateDirtyState();
}
function wireCards() {
$('acctList').querySelectorAll('.acct-card').forEach(card => {
const head = card.querySelector('.acct-head'), body = card.querySelector('.acct-body');
head.addEventListener('click', (e) => { if (e.target.closest('.addr-pill')) return; card.classList.toggle('open'); body.classList.toggle('hidden'); });
card.querySelectorAll('input.f-in, .raw-off, .raw-hex').forEach(inp => inp.addEventListener('input', () => {
if (inp.classList.contains('f-in')) inp.classList.toggle('dirty', inp.value.trim() !== inp.dataset.cur);
updateDirtyState();
}));
});
}
function updateDirtyState() {
let total = 0;
$('acctList').querySelectorAll('.acct-card').forEach(card => {
const df = card.querySelectorAll('input.f-in.dirty').length;
const ro = card.querySelector('.raw-off'), rh = card.querySelector('.raw-hex');
const rawSet = (ro && ro.value.trim() !== '') || (rh && rh.value.trim() !== '');
const has = df > 0 || rawSet;
card.classList.toggle('has-edits', has);
card.querySelector('.edit-dot').classList.toggle('hidden', !has);
if (has) total += df + (rawSet ? 1 : 0);
});
$('dirtyCount').textContent = total ? `${total} unstaged edit${total > 1 ? 's' : ''}` : '';
}
function collectAllEdits() {
const out = [];
$('acctList').querySelectorAll('.acct-card').forEach(card => {
const address = card.dataset.address, a = mutableAccounts.find(x => x.address === address);
card.querySelectorAll('input.f-in').forEach(inp => {
const v = inp.value.trim(); if (v === inp.dataset.cur) return;
if (inp.dataset.kind === 'lamports') out.push({ kind: 'lamports', address, lamports: Number(v), _label: `lamports → ${fmt(v)}` });
else { const f = a.decoded.fields[Number(inp.dataset.fi)]; out.push({ kind: 'data', address, offset: f.offset, bytes_hex: encodeField(f, v), _label: `${f.name} → ${v}` }); }
});
const off = card.querySelector('.raw-off'), hx = card.querySelector('.raw-hex');
if (off && hx && (off.value.trim() !== '' || hx.value.trim() !== '')) {
const o = Number(off.value), h = hx.value.trim().replace(/^0x/, '').replace(/[\s_]/g, '');
if (!Number.isInteger(o) || o < 0) throw new Error(`${short(address)}: raw offset must be a non-negative integer`);
if (!/^([0-9a-fA-F]{2})+$/.test(h)) throw new Error(`${short(address)}: raw bytes must be even-length hex`);
out.push({ kind: 'data', address, offset: o, bytes_hex: h, _label: `@${o} ← 0x${h}` });
}
});
return out;
}
function stageAll() {
try {
const edits = collectAllEdits();
if (!edits.length) { flashSim('nothing changed to stage', true); return; }
mutations.push(...edits); renderStaged();
$('acctList').querySelectorAll('input.f-in.dirty').forEach(i => { i.value = i.dataset.cur; i.classList.remove('dirty'); });
$('acctList').querySelectorAll('.raw-off, .raw-hex').forEach(i => { i.value = ''; });
updateDirtyState(); flashSim('');
} catch (e) { flashSim(e.message, true); }
}
function renderStaged() {
const wrap = $('stagedWrap');
if (!mutations.length) { wrap.classList.add('hidden'); $('staged').innerHTML = ''; return; }
wrap.classList.remove('hidden');
$('staged').innerHTML = mutations.map((m, i) =>
`<div class="staged-item"><span class="k">${m.kind === 'lamports' ? 'LAMPORTS' : 'DATA'}</span>
<span class="body">${copyPill(m.address)} ${esc(m._label || '')}</span>
<button class="rm" data-i="${i}" title="remove">✕</button></div>`).join('');
$('staged').querySelectorAll('.rm').forEach(b => b.addEventListener('click', () => { mutations.splice(Number(b.dataset.i), 1); renderStaged(); }));
wireCopy($('staged'));
}
function filterAccounts(q) {
q = q.trim().toLowerCase();
$('acctList').querySelectorAll('.acct-card').forEach(card => card.classList.toggle('hidden', q !== '' && !card.dataset.key.includes(q)));
}
function toggleExpandAll() {
const cards = [...$('acctList').querySelectorAll('.acct-card:not(.hidden)')];
const anyClosed = cards.some(c => !c.classList.contains('open'));
cards.forEach(c => { c.classList.toggle('open', anyClosed); c.querySelector('.acct-body').classList.toggle('hidden', !anyClosed); });
$('expandAll').textContent = anyClosed ? 'Collapse all' : 'Expand all';
}
function flashSim(msg, isErr) { $('simResult').innerHTML = msg ? `<div class="status ${isErr ? 'err' : ''}" style="text-align:left;margin:10px 0">${esc(msg)}</div>` : ''; }
// ---------------- actions ----------------
function wireCopy(root) {
root.querySelectorAll('.addr-pill').forEach(el => {
if (el._wired) return; el._wired = true;
el.addEventListener('click', async (e) => {
e.stopPropagation();
try { await navigator.clipboard.writeText(el.dataset.copy); } catch {}
const t = el.textContent; el.classList.add('copied'); el.textContent = 'copied';
setTimeout(() => { el.textContent = t; el.classList.remove('copied'); }, 900);
});
});
}
// Decide what the pasted value is: a 32-byte value is an account/program address
// (show its recent transactions), a 64-byte value is a signature (analyze it).
function runInput() {
const input = $('sig').value.trim();
if (!input) return;
let len = null;
try { len = bs58decode(input).length; } catch { /* not base58 */ }
if (len === 32) return showAddress(input); // account / program address
if (len === 64) return analyze(); // transaction signature
// Anything else never reaches the RPC — a friendly nudge instead of a raw
// "-32602 WrongSize" bubbling up from the backend.
$('hero').classList.add('hidden'); $('out').classList.add('hidden'); $('txlist').classList.add('hidden');
const st = $('status'); st.className = 'status err'; st.classList.remove('hidden');
st.textContent = "That doesn't look like a Solana signature or address — paste a full transaction signature (~88 chars) or an account / program address (~44 chars).";
}
// Show an explorer-style address page: the account/program overview + its recent
// transactions to pick from.
async function showAddress(address) {
$('go').disabled = true;
$('out').classList.add('hidden'); $('hero').classList.add('hidden'); $('txlist').classList.add('hidden');
const st = $('status'); st.className = 'status'; st.classList.remove('hidden');
st.innerHTML = '<span class="spinner"></span>loading account…';
try {
const [account, sigs] = await Promise.all([
api(`/account/${encodeURIComponent(address)}` + clusterQ()).then(async r => { if (!r.ok) throw new Error(await r.text()); return r.json(); }),
api(`/signatures/${encodeURIComponent(address)}` + clusterQ()).then(r => r.ok ? r.json() : []),
]);
st.classList.add('hidden');
renderAddressPage(address, account, sigs);
} catch (e) {
st.className = 'status err'; st.classList.remove('hidden'); st.textContent = 'error: ' + e.message;
} finally { $('go').disabled = false; }
}
function timeAgo(t) {
if (!t) return '';
const s = Math.max(0, Math.floor(Date.now() / 1000 - t));
if (s < 60) return s + 's ago';
if (s < 3600) return Math.floor(s / 60) + 'm ago';
if (s < 86400) return Math.floor(s / 3600) + 'h ago';
return Math.floor(s / 86400) + 'd ago';
}
function ovRow(k, v) { return `<div class="ov-row"><span class="ovk">${esc(k)}</span><span class="ovv">${v}</span></div>`; }
function renderAddressPage(address, a, sigs) {
const box = $('txlist'); box.classList.remove('hidden');
let header;
if (!a || !a.exists) {
header = `<div class="acct-ov"><div class="ov-name">Account not found</div>
<div class="ov-sub">${esc(short(address))} doesn't exist on this cluster.</div></div>`;
} else if (a.executable) {
const p = a.program || {};
const rows = [
ovRow('Address', copyPill(a.address, a.address)),
ovRow('Balance', fmtSol(a.lamports)),
ovRow('Executable', 'Yes'),
p.program_data ? ovRow('Program Data', copyPill(p.program_data)) : '',
ovRow('Upgradeable', p.upgradeable ? 'Yes' : 'No (immutable)'),
p.upgrade_authority ? ovRow('Upgrade Authority', copyPill(p.upgrade_authority)) : '',
p.last_deployed_slot != null ? ovRow('Last Deployed Slot', fmt(p.last_deployed_slot)) : '',
].join('');
header = `<div class="acct-ov">
<div class="ov-tag">${a.idl_name ? 'Program · IDL' : 'Program'}</div>
<div class="ov-name">${esc(a.idl_name || short(address))}</div>${rows}</div>`;
} else {
const rows = [
ovRow('Address', copyPill(a.address, a.address)),
ovRow('Owner', progPill(a.owner)),
ovRow('Balance', fmtSol(a.lamports)),
ovRow('Data size', a.data_len + ' bytes'),
].join('');
const fields = a.decoded ? a.decoded.fields.map(f =>
`<div class="ov-row"><span class="ovk">${esc(f.name)} <span class="ovoff">${esc(f.type)}</span></span>` +
`<span class="ovv" style="word-break:break-all">${esc(f.value)}</span></div>`).join('') : '';
header = `<div class="acct-ov">
<div class="ov-tag">${esc(a.decoded ? a.decoded.type_name : 'Account')}</div>
<div class="ov-name">${esc(short(address))}</div>${rows}
${fields ? `<div class="ov-fields-label">decoded fields</div>${fields}` : ''}</div>`;
}
const list = sigs.length
? `<h2>Recent transactions · ${sigs.length}</h2>` + sigs.map(s =>
`<div class="txrow" data-sig="${esc(s.signature)}">
<span class="st ${s.err ? 'err' : 'ok'}"></span>
<span class="sig">${esc(s.signature)}</span>
${s.slot != null ? `<span class="slot">slot ${fmt(s.slot)}</span>` : ''}
<span class="when">${esc(timeAgo(s.block_time))}</span>
<span class="go">analyze →</span>
</div>`).join('')
: '<div class="empty" style="padding:10px 2px">no transactions</div>';
box.innerHTML = header + list;
box.querySelectorAll('.txrow').forEach(row => row.addEventListener('click', () => {
$('sig').value = row.dataset.sig;
analyze();
}));
wireCopy(box);
}
async function analyze() {
const sig = $('sig').value.trim(); if (!sig) return;
currentSig = sig; mutations = [];
$('go').disabled = true; $('txlist').classList.add('hidden'); $('out').classList.add('hidden'); $('hero').classList.add('hidden');
const st = $('status'); st.className = 'status'; st.classList.remove('hidden');
st.innerHTML = '<span class="spinner"></span>decoding transaction…';
// If the hosted instance was idle it has to boot; say so rather than look hung.
const slow = setTimeout(() => {
st.innerHTML = '<span class="spinner"></span>waking the simulation engine — this can take ~30s on the free demo…';
}, 4000);
try {
const res = await api(`/analyze/${encodeURIComponent(sig)}` + clusterQ()).finally(() => clearTimeout(slow));
if (!res.ok) throw new Error(await res.text());
analysis = await res.json();
// The input may have been an address we resolved to its latest transaction —
// adopt the resolved signature so replay/simulate/freeze use the real tx.
if (analysis.signature && analysis.signature !== sig) {
currentSig = analysis.signature;
$('sig').value = analysis.signature;
}
renderOverview(analysis.overview, analysis.compute);
renderTree(analysis.cpi_tree);
renderCompute(analysis.compute);
renderDiffs(analysis.balance_change);
renderTokens(analysis.token_change);
renderOnchainLogs(analysis.logs);
renderReplayPanel();
renderStaged(); flashSim('');
renderAccountList();
buildSuite(); renderSuite();
$('exportBox').classList.add('hidden'); $('suiteResult').textContent = '';
$('acctFilter').value = ''; $('expandAll').textContent = 'Expand all';
wireCopy($('out'));
st.classList.add('hidden'); $('out').classList.remove('hidden');
} catch (e) { st.className = 'status err'; st.textContent = 'error: ' + e.message; }
finally { $('go').disabled = false; }
}
async function simulate() {
if (!analysis) { flashSim('analyze a transaction first', true); return; }
let toSend;
try { toSend = [...mutations, ...collectAllEdits()]; } catch (e) { flashSim(e.message, true); return; }
if (!toSend.length) { flashSim('change at least one field, or stage a mutation', true); return; }
const btn = $('simBtn'); btn.disabled = true;
$('simResult').innerHTML = '<div class="status" style="text-align:left;margin:12px 0"><span class="spinner"></span>replaying with mutations…</div>';
try {
// Need the un-mutated baseline for the before/after comparison; fetch it lazily.
const before = await ensureBaseline();
const res = await api('/simulate', { method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ signature: currentSig, mutations: toSend.map(({ _label, ...m }) => m), time_travel: timeTravel, features: activeFeatures, ...clusterParams() }) });
if (!res.ok) throw new Error(await res.text());
showResult(before, await res.json(), { n: toSend.length });
} catch (e) { flashSim('error: ' + e.message, true); }
finally { btn.disabled = false; }
}
// One result renderer for both quick scenarios and the advanced editor.
// Leads with a plain-English verdict, then the original→mutated comparison.
function showResult(before, after, opts) {
const flipped = before.success !== after.success;
let ic, cls, vt, vs;
if (after.success) {
ic = '✅'; cls = 'ok'; vt = flipped ? 'It would now succeed' : 'It still succeeds';
vs = flipped ? 'Your change turned a failing transaction into a passing one.'
: 'The transaction still goes through after your change.';
} else {
ic = '❌'; cls = 'bad'; vt = flipped ? 'It would now fail' : 'It still fails';
vs = 'Reason: ' + friendlyReason(after) + '.';
}
const chip = (r) => r.success ? '<span class="badge ok"><span class="b-dot"></span>success</span>' : '<span class="badge fail"><span class="b-dot"></span>failed</span>';
const dCU = after.compute_units - before.compute_units;
const dc = dCU > 0 ? 'up' : dCU < 0 ? 'down' : 'same';
const dt = dCU === 0 ? 'no change in compute' : `${dCU > 0 ? '+' : ''}${fmt(dCU)} CU vs. original`;
const head = opts.scenarioQ
? `What-if · <span style="color:var(--text)">${esc(opts.scenarioQ)}</span>`
: `Mutated replay · ${opts.n} mutation${opts.n > 1 ? 's' : ''} applied`;
$('simResult').innerHTML = `
<div class="verdict ${cls}"><div class="ic">${ic}</div><div><div class="vt">${vt}</div><div class="vs">${esc(vs)}</div></div></div>
<div style="font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--muted);margin:2px 0 10px;font-weight:600">${head}</div>
<div class="compare">
<div class="compare-col"><div class="lbl">Original transaction</div>${chip(before)}<div class="cu-note" style="margin-top:8px"><b>${fmt(before.compute_units)}</b> CU</div></div>
<div class="arrow">→</div>
<div class="compare-col"><div class="lbl">With your change</div>${chip(after)}<div class="cu-note" style="margin-top:8px"><b>${fmt(after.compute_units)}</b> CU</div>
<div class="delta ${dc}">${dt}</div></div>
</div>
${after.error ? `<div class="err-box">${esc(after.error)}</div>` : ''}
${after.logs && after.logs.length ? `<div class="logs">${renderLogs(after.logs)}</div>` : ''}`;
$('simResult').scrollIntoView({ block: 'nearest' });
}
// ===========================================================================
// Pre-flight simulator: simulate an unsigned tx, or build one from an IDL.
// ===========================================================================
// base58 encode (for building instructions we need the reverse of bs58decode)
function bs58encode(bytes) {
if (!bytes.length) return '';
const digits = [0];
for (const b of bytes) {
let carry = b;
for (let i = 0; i < digits.length; i++) { carry += digits[i] << 8; digits[i] = carry % 58; carry = (carry / 58) | 0; }
while (carry) { digits.push(carry % 58); carry = (carry / 58) | 0; }
}
let out = '';
for (const b of bytes) { if (b === 0) out += '1'; else break; }
for (let i = digits.length - 1; i >= 0; i--) out += B58[digits[i]];
return out;
}
function showSim(on) {
$('sim').classList.toggle('hidden', !on);
// Keep whatever analysis is on screen when leaving Simulate, so switching tabs
// doesn't throw away the user's work.
const hasResults = !!analysis;
$('hero').classList.toggle('hidden', on || hasResults);
if (on) {
$('out').classList.add('hidden'); $('txlist').classList.add('hidden'); $('status').classList.add('hidden');
} else if (hasResults) {
$('out').classList.remove('hidden');
}
$('navSim').classList.toggle('on', on);
$('navAnalyze').classList.toggle('on', !on);
}
let loadedIx = []; // instructions from the loaded program's IDL
async function loadInstructions() {
const prog = $('buildProg').value.trim();
if (!prog) return;
const btn = $('loadIx'); btn.disabled = true; btn.textContent = 'Loading…';
$('simOut').innerHTML = '';
try {
const res = await api(`/instructions/${encodeURIComponent(prog)}` + clusterQ());
if (!res.ok) throw new Error(await res.text());
loadedIx = await res.json();
if (!loadedIx.length) throw new Error('this program publishes no instructions');
showLoadedInstructions();
$('idlFallback').classList.add('hidden');
} catch (e) {
$('simOut').innerHTML = `<div class="status err" style="text-align:left">${esc(e.message)}</div>`;
$('ixPicker').classList.add('hidden');
// No on-chain IDL? Reveal the paste-your-own-IDL fallback the message promises.
if (/no on-chain Anchor IDL|no instructions/i.test(e.message)) $('idlFallback').classList.remove('hidden');
} finally { btn.disabled = false; btn.textContent = 'Load instructions'; }
}
// Populate the instruction picker from whatever's in `loadedIx`.
function showLoadedInstructions() {
$('ixSelect').innerHTML = loadedIx.map((ix, i) => `<option value="${i}">${esc(ix.name)}</option>`).join('');
$('ixPicker').classList.remove('hidden');
$('ixHint').classList.add('hidden');
renderIxForm();
}
// Load instructions from a pasted Anchor IDL (no on-chain publish needed).
async function loadInstructionsFromIdl() {
const raw = ($('idlJson').value || '').trim();
if (!raw) { $('simOut').innerHTML = '<div class="status err" style="text-align:left">paste the IDL JSON first</div>'; return; }
let idl;
try { idl = JSON.parse(raw); } catch { $('simOut').innerHTML = '<div class="status err" style="text-align:left">that isn\'t valid JSON</div>'; return; }
const btn = $('useIdl'); btn.disabled = true; btn.textContent = 'Loading…';
$('simOut').innerHTML = '';
try {
const res = await api('/idl_instructions', { method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ idl, ...clusterParams() }) });
if (!res.ok) throw new Error(await res.text());
loadedIx = await res.json();
if (!loadedIx.length) throw new Error('no instructions found in that IDL');
showLoadedInstructions();
} catch (e) {
$('simOut').innerHTML = `<div class="status err" style="text-align:left">${esc(e.message)}</div>`;
} finally { btn.disabled = false; btn.textContent = 'Load instructions from IDL'; }
}
// Render inputs for the selected instruction's accounts and args.
// ---- PDA derivation (so users never paste a derived address) ----
// A program address must be *off* the ed25519 curve; findProgramAddress walks the
// bump downward until it finds one, exactly like the on-chain implementation.
const PDA_MARKER = new TextEncoder().encode('ProgramDerivedAddress');
async function sha256(bytes) {
const d = await crypto.subtle.digest('SHA-256', new Uint8Array(bytes));
return new Uint8Array(d);
}
/** Is this 32-byte value a valid ed25519 point? (decompression check) */
function isOnCurve(p) {
const P = (1n << 255n) - 19n;
const D = -121665n * modInv(121666n, P) % P;
const bytes = [...p];
const sign = bytes[31] >> 7;
bytes[31] &= 0x7f;
let y = 0n;
for (let i = 31; i >= 0; i--) y = (y << 8n) | BigInt(bytes[i]);
if (y >= P) return false;
const y2 = y * y % P;
const u = (y2 - 1n + P) % P;
const v = (D * y2 % P + 1n) % P;
// x = sqrt(u/v) exists iff (u/v)^((P-1)/2) == 1
const uv = u * modInv(v, P) % P;
let x = modPow(uv, (P + 3n) / 8n, P);
if (x * x % P !== uv) x = x * modPow(2n, (P - 1n) / 4n, P) % P;
if (x * x % P !== uv) return false;
if (x === 0n && sign === 1) return false;
return true;
}
function modPow(b, e, m) { let r = 1n; b %= m; while (e > 0n) { if (e & 1n) r = r * b % m; b = b * b % m; e >>= 1n; } return r; }
function modInv(a, m) { return modPow(((a % m) + m) % m, m - 2n, m); }
/** Derive a PDA from seeds + program id; returns base58 or null. */
async function findProgramAddress(seedByteArrays, programId) {
const pid = bs58decode(programId);
for (let bump = 255; bump >= 0; bump--) {
const buf = [];
for (const s of seedByteArrays) buf.push(...s);
buf.push(bump, ...pid, ...PDA_MARKER);
const h = await sha256(buf);
if (!isOnCurve(h)) return bs58encode([...h]);
}
return null;
}
/** Fill any PDA account inputs whose seed accounts are all known. */
async function autoDerivePdas() {
const ix = loadedIx[Number($('ixSelect').value)];
const program = $('buildProg').value.trim();
if (!ix || !program) return;
const inputs = [...$('ixForm').querySelectorAll('.ixacct')];
const valueOf = (name) => {
const i = ix.accounts.findIndex(a => a.name === name);
return i >= 0 ? inputs[i]?.value.trim() : '';
};
for (let i = 0; i < ix.accounts.length; i++) {
const spec = ix.accounts[i];
if (!spec.seeds?.length) continue;
const el = inputs[i];
if (el.dataset.userEdited === '1') continue; // never clobber a manual entry
const seeds = [];
let ok = true;
for (const sd of spec.seeds) {
if (sd.kind === 'const') { seeds.push(sd.bytes); continue; }
const v = valueOf(sd.path);
if (!v) { ok = false; break; }
try { seeds.push(bs58decode(v)); } catch { ok = false; break; }
}
if (!ok) continue;
try {
const pda = await findProgramAddress(seeds, program);
if (pda) { el.value = pda; el.classList.add('derived'); el.title = 'derived from seeds'; }
} catch { /* leave it for manual entry */ }
}
}
function renderIxForm() {
const ix = loadedIx[Number($('ixSelect').value)];
if (!ix) return;
const docs = ix.docs?.length ? `<p class="ixdoc">${esc(ix.docs.join(' '))}</p>` : '';
const accounts = ix.accounts.map((a, i) => `
<div class="ixrow">
<div><span class="nm">${esc(a.name)}</span><span class="tags">
${a.signer ? '<span class="ixtag s">signer</span>' : ''}
${a.writable ? '<span class="ixtag w">writable</span>' : ''}
${a.pda ? '<span class="ixtag">pda</span>' : ''}</span></div>
<input class="finput mono ixacct${a.seeds?.length ? ' pdaField' : ''}" data-i="${i}" spellcheck="false"
value="${esc(a.address || '')}"
placeholder="${a.seeds?.length ? 'derived automatically from seeds…' : 'account address…'}" />
</div>`).join('');
const args = ix.args.length ? ix.args.map((a, i) => `
<div class="ixrow">
<div><span class="nm">${esc(a.name)}</span><span class="tags"><span class="ixtag">${esc(a.type)}</span></span></div>
<input class="finput mono ixarg" data-i="${i}" data-type="${esc(a.type)}" spellcheck="false" placeholder="${esc(a.type)} value…" />
</div>`).join('') : '<div class="empty">no arguments</div>';
$('ixForm').innerHTML = `${docs}
<div class="ixsec">Accounts</div>${accounts}
<div class="ixsec">Arguments</div>${args}
<div class="ixsec">Fee payer & signer</div>
<div class="ixrow"><div><span class="nm">payer</span></div>
<input class="finput mono" id="ixPayer" spellcheck="false" placeholder="wallet address that pays the fee…" /></div>`;
// Derive PDAs as their seed accounts get filled in, so the user never has to
// paste an address the IDL already knows how to compute.
$('ixForm').querySelectorAll('.ixacct').forEach(el => {
el.addEventListener('input', () => {
if (!el.classList.contains('pdaField')) autoDerivePdas();
else { el.dataset.userEdited = el.value.trim() ? '1' : '0'; el.classList.remove('derived'); }
});
});
// A payer is usually also a seed account (signer), so derive from it too.
$('ixPayer').addEventListener('input', () => {
$('ixForm').querySelectorAll('.ixacct').forEach(el => {
const i = Number(el.dataset.i);
const spec = loadedIx[Number($('ixSelect').value)]?.accounts[i];
if (spec?.signer && !el.value.trim()) el.value = $('ixPayer').value.trim();
});
autoDerivePdas();
});
autoDerivePdas();
}
// Encode an instruction argument to Borsh bytes.
function encodeArg(type, raw) {
const v = (raw ?? '').trim();
const le = (bi, n) => { const o = []; let x = BigInt(bi); for (let i = 0; i < n; i++) { o.push(Number(x & 0xffn)); x >>= 8n; } return o; };
switch (type) {
case 'u8': return [Number(v) & 0xff];
case 'u16': return le(v || 0, 2);
case 'u32': return le(v || 0, 4);
case 'u64': return le(v || 0, 8);
case 'u128': return le(v || 0, 16);
case 'i8': case 'i16': case 'i32': case 'i64': case 'i128': {
const bits = { i8: 8, i16: 16, i32: 32, i64: 64, i128: 128 }[type];
let x = BigInt(v || 0); if (x < 0n) x += 1n << BigInt(bits);
return le(x, bits / 8);
}
case 'bool': return [/^(1|true|yes)$/i.test(v) ? 1 : 0];
case 'pubkey': case 'publicKey': return bs58decode(v);
case 'string': { const b = [...new TextEncoder().encode(v)]; return [...le(b.length, 4), ...b]; }
default: throw new Error(`unsupported argument type "${type}" — use the paste tab`);
}
}
// Build a legacy VersionedTransaction (unsigned) from the form, as base64.
function buildTxB64() {
const ix = loadedIx[Number($('ixSelect').value)];
if (!ix) throw new Error('pick an instruction');
const payer = $('ixPayer').value.trim();
if (!payer) throw new Error('enter a fee payer address');
const program = $('buildProg').value.trim();
const accts = [...$('ixForm').querySelectorAll('.ixacct')].map((el, i) => {
const addr = el.value.trim();
if (!addr) throw new Error(`account "${ix.accounts[i].name}" is required`);
return { addr, signer: ix.accounts[i].signer, writable: ix.accounts[i].writable };
});
// instruction data = discriminator + borsh args
let data = [...ix.discriminator];
[...$('ixForm').querySelectorAll('.ixarg')].forEach((el, i) => {
data = data.concat(encodeArg(el.dataset.type, el.value));
});
// Assemble the account key list: payer first (signer+writable), then the
// instruction's accounts, then the program id — ordered signers → writables.
const keys = new Map();
const put = (addr, signer, writable) => {
const cur = keys.get(addr) || { signer: false, writable: false };
keys.set(addr, { signer: cur.signer || signer, writable: cur.writable || writable });
};
put(payer, true, true);
accts.forEach(a => put(a.addr, a.signer, a.writable));
put(program, false, false);
const entries = [...keys.entries()];
const order = [
...entries.filter(([, m]) => m.signer && m.writable),
...entries.filter(([, m]) => m.signer && !m.writable),
...entries.filter(([, m]) => !m.signer && m.writable),
...entries.filter(([, m]) => !m.signer && !m.writable),
];
const idxOf = a => order.findIndex(([k]) => k === a);
const numSigners = order.filter(([, m]) => m.signer).length;
const numReadonlySigned = order.filter(([, m]) => m.signer && !m.writable).length;
const numReadonlyUnsigned = order.filter(([, m]) => !m.signer && !m.writable).length;
const shortvec = n => { const o = []; let x = n; do { let b = x & 0x7f; x >>= 7; if (x) b |= 0x80; o.push(b); } while (x); return o; };
const msg = [
numSigners, numReadonlySigned, numReadonlyUnsigned,
...shortvec(order.length),
...order.flatMap(([k]) => bs58decode(k)),
...new Array(32).fill(0), // recent blockhash (replay ignores it)
...shortvec(1), // one instruction
idxOf(program),
...shortvec(accts.length),
...accts.map(a => idxOf(a.addr)),
...shortvec(data.length),
...data,
];
// legacy transaction: signature count + blank signatures + message
const tx = [...shortvec(numSigners), ...new Array(64 * numSigners).fill(0), ...msg];
let bin = ''; for (const b of tx) bin += String.fromCharCode(b);
return btoa(bin);
}
// ---- time travel ----
let timeTravel = {}; // sent as `time_travel`; {} = simulate at the current time
function describeTT(tt) {
if (!tt || !Object.keys(tt).length) return 'Simulating at the current time.';
const parts = [];
const plural = (n, w) => `${n} ${w}${Math.abs(n) === 1 ? '' : 's'}`;
if (tt.epochs) parts.push(plural(tt.epochs, 'epoch'));
if (tt.slots) parts.push(plural(tt.slots, 'slot'));
if (tt.seconds) {
const s = tt.seconds;
if (s % 86400 === 0) parts.push(plural(s / 86400, 'day'));
else if (s % 3600 === 0) parts.push(plural(s / 3600, 'hour'));
else if (s % 60 === 0) parts.push(plural(s / 60, 'minute'));
else parts.push(plural(s, 'second'));
}
if (tt.at_unix_timestamp) parts.push(`to ${new Date(tt.at_unix_timestamp * 1000).toISOString().slice(0, 16).replace('T', ' ')} UTC`);
if (tt.at_epoch != null) parts.push(`to epoch ${tt.at_epoch}`);
if (tt.at_slot != null) parts.push(`to slot ${fmt(tt.at_slot)}`);
const how = parts.join(' + ');
if (how.startsWith('to ')) return `Simulating ${how}.`;
// Negative offsets travel to the past; the sign lives in the number, so read it.
const back = /(^|\s)-/.test(how);
return `Simulating ${how.replace('-', '')} ${back ? 'into the past' : 'into the future'}.`;
}
function setTimeTravel(tt, chipEl) {
timeTravel = tt || {};
const warped = !!Object.keys(timeTravel).length;
document.querySelectorAll('.chip.gtt').forEach(c =>
c.classList.toggle('on', chipEl ? c === chipEl : (!warped && c.dataset.tt === '{}')));
const text = describeTT(timeTravel);
$('ttState').textContent = text;
$('ttState').classList.toggle('warped', warped);
// Header button reflects the current clock at a glance.
$('clockLabel').textContent = warped ? '⏱ ' + shortTT(timeTravel) : '⏱ Now';
$('clockBtn').classList.toggle('warped', warped);
// A page-wide banner so the shifted clock is never a surprise.
const b = $('clockBanner');
b.classList.toggle('hidden', !warped);
if (warped) {
b.innerHTML = `<span>⏱</span><span>${esc(text)} Everything below — replays, what-ifs and pre-flight — runs at this time.</span>
<button class="rst" id="clockReset">reset to now</button>`;
$('clockReset').addEventListener('click', () => setTimeTravel({}, null));
}
// Re-run whatever is on screen so results match the selected clock.
if (typeof onClockChanged === 'function') onClockChanged();
}
/** Compact label for the header button, e.g. "+30d" or "−5d" or "+1 epoch". */
function shortTT(tt) {
const sign = n => (n < 0 ? '−' : '+'); // real minus glyph, never "+-"
if (tt.epochs) return `${sign(tt.epochs)}${Math.abs(tt.epochs)} epoch${Math.abs(tt.epochs) === 1 ? '' : 's'}`;
if (tt.slots) return `${sign(tt.slots)}${fmt(Math.abs(tt.slots))} slots`;
if (tt.seconds) {
const a = Math.abs(tt.seconds), sg = sign(tt.seconds);
if (a % 31536000 === 0) return `${sg}${a / 31536000}y`;
if (a % 86400 === 0) return `${sg}${a / 86400}d`;
if (a % 3600 === 0) return `${sg}${a / 3600}h`;
return `${sg}${Math.round(a / 60)}m`;
}
if (tt.at_unix_timestamp) return new Date(tt.at_unix_timestamp * 1000).toISOString().slice(0, 10);
return 'shifted';
}
function applyCustomTT() {
// Time travel is forward-only (vesting cliffs, cooldowns, auctions all lie
// ahead); a stray minus sign just means "that many into the future".
const n = Math.abs(Number($('ttAmount').value));
if (!n) { setTimeTravel({}, null); return; }
const unit = $('ttUnit').value;
const mult = { minutes: 60, hours: 3600, days: 86400 };
const tt = unit === 'epochs' ? { epochs: n }
: unit === 'slots' ? { slots: n }
: { seconds: n * mult[unit] };
setTimeTravel(tt, null);
}
/** When the clock changes, refresh the replay result if one is showing. */
function onClockChanged() {
if (analysis && !$('out').classList.contains('hidden')) renderReplayPanel();
}
async function runPreflight() {
const btn = $('runSim'); btn.disabled = true;
$('simOut').innerHTML = '<div class="status" style="text-align:left"><span class="spinner"></span>simulating against live state…</div>';
try {
const building = !$('tabBuild').classList.contains('hidden');
const b64 = building ? buildTxB64() : $('txB64').value.trim();
if (!b64) throw new Error('paste a base64 transaction first');
const res = await api('/preflight_report', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ transaction: b64, mutations: [], time_travel: timeTravel, features: activeFeatures, ...clusterParams() }),
});
if (!res.ok) throw new Error(await res.text());
renderReport(await res.json(), $('simOut'));
} catch (e) {
$('simOut').innerHTML = `<div class="status err" style="text-align:left">${esc(e.message)}</div>`;
} finally { btn.disabled = false; }
}
// Shared renderer: verdict + human explanation + account diff + logs.
function renderReport(rep, el) {
const r = rep.replay;
const when = rep.clock ? ` · <b>${esc(rep.clock)}</b>` : '';
const head = r.success
? `<div class="verdict ok"><div class="ic">✅</div><div><div class="vt">This transaction would succeed</div>
<div class="vs">Ran against live state · ${fmt(r.compute_units)} compute units${when}</div></div></div>`
: (rep.clock ? `<div class="status" style="text-align:left;margin-bottom:10px">simulated at <b>${esc(rep.clock)}</b></div>` : '');
const ex = rep.explain
? `<div class="explain"><div class="ic">⚠️</div><div>
<div class="t">${esc(rep.explain.title)}</div>
<div class="d">${esc(rep.explain.detail)}</div>
<div class="meta">${esc(rep.explain.raw)}${rep.explain.program ? ' · ' + esc(short(rep.explain.program)) : ''}</div>
</div></div>`
: '';
const diffs = (rep.diffs || []).length ? `
<div class="card" style="margin-top:14px">
<h2>Account changes <span class="count">${rep.diffs.length}</span></h2>
<div class="diffs">${rep.diffs.map(d => {
const dl = d.lamports_after - d.lamports_before;
const lam = dl !== 0
? `<span class="lam ${dl > 0 ? 'amt pos' : 'amt neg'}">${dl > 0 ? '+' : '−'}${fmtSol(Math.abs(dl))}</span>` : '';
const rows = d.fields.map(f => `
<div class="drow"><span class="fname">${esc(f.name)} <span class="f-off">${esc(f.type)}</span></span>
<span class="fv"><span class="old">${esc(f.before)}</span><span class="arr">→</span><span class="new">${esc(f.after)}</span></span>
</div>`).join('');
const raw = d.raw_data_changed ? '<div class="drow"><span class="fname">data</span><span class="fv"><span class="new">changed (layout not decoded)</span></span></div>' : '';
return `<div class="dcard"><div class="dhead">${copyPill(d.address)}${progPill(d.owner)}${lam}</div>${rows}${raw}</div>`;
}).join('')}</div>
</div>` : '';
const logs = r.logs?.length ? `<div class="card" style="margin-top:14px"><h2>Program logs</h2><div class="logs">${renderLogs(r.logs)}</div></div>` : '';
el.innerHTML = head + ex + diffs + logs;
wireCopy(el);
}
document.querySelectorAll('.chip.gtt').forEach(c =>
c.addEventListener('click', () => { $('ttAmount').value = ''; setTimeTravel(JSON.parse(c.dataset.tt), c); }));
$('ttApply').addEventListener('click', applyCustomTT);
$('ttAmount').addEventListener('keydown', e => { if (e.key === 'Enter') applyCustomTT(); });
$('clockBtn').addEventListener('click', e => { e.stopPropagation(); $('clockPop').classList.toggle('hidden'); $('featPop').classList.add('hidden'); });
document.addEventListener('click', e => {
if (!e.target.closest('.clockctl')) { $('clockPop').classList.add('hidden'); $('featPop').classList.add('hidden'); }
});
document.addEventListener('keydown', e => {
if (e.key === 'Escape') { $('clockPop').classList.add('hidden'); $('featPop').classList.add('hidden'); }
});
// ---- feature gates ----
// Curated, mainnet-active Solana feature gates worth toggling. Each is "on" by
// default (it's live); un-ticking sends {active:false} to replay without it. Any
// other gate can be added by pubkey. Verified live: deactivating the get_sysvar
// syscall gate makes a Jupiter swap fail (it depends on that syscall).
const ALL_GATES=[{id:"FuS3FPfJDKSNot99ECLXtp3rueq36hMNStJkPJwWodLh",label:"abort on invalid curve"},{id:"SVn36yVApPLYsa8koK3qUcy14zXDnqkNYWyUh1f4oK1",label:"account hash ignore slot"},{id:"LTHasHQX6661DaDD4S6A2TFi6QBuiwXKv66fB1obfHq",label:"accounts lt hash"},{id:"4d5AKtxoh93Dwm1vHXUU3iRATuMndx1c431KgT2td52r",label:"add compute budget program"},{id:"St8k9dVXP97xT6faW24YmRSYConLbhsMJA4TJTBLmMT",label:"add get minimum delegation instruction to stake program"},{id:"CFK1hRCNy8JJuAAY8Pb2GjLFNdCThS2qwZNe3izzBMgn",label:"add get processed sibling instruction syscall"},{id:"8U4skmMVnF6k2kMvrWbQuRUT3qQSiTYpSjqmhmgfthZu",label:"add new reserved account keys"},{id:"98std1NSHqXi9WYvFShfVepRdCoq1qvsp8fsR2XZtG8g",label:"add set compute unit price ix"},{id:"G6vbf1UBok8MWb8m25ex86aoQHeKTzDKzuZADHkShqm6",label:"add set tx loaded accounts data size instruction"},{id:"Ds87KVeqhbv7Jw8W6avsS1mqz3Mw5J3pRTpPoDQ2QdiJ",label:"add shred type to shred seed"},{id:"decoMktMcnmiq6t3u7g5BfgcQu91nKZr6RvMYf9z1Jb",label:"allow commission decrease at any time"},{id:"Ff8b1fBeB86q8cjq47ZhsQLgv5EkHu3G1C99zjUfAzrq",label:"allow votes to directly update vote state"},{id:"bn2oPgpkzQPT3tohMaAsMVGjhDmmDa4jCaVPqCFmtxM",label:"alt bn128 little endian"},{id:"2ry7ygxiYURULZCrypHhveanvP5tzZ4toRwVp89oCNSj",label:"apply cost tracker during replay"},{id:"Vo5siZ442SaZBKPXNocthiXysNviW4UYPwRFggmbgAp",label:"bank transaction count fix"},{id:"Ffswd3egL3tccB6Rv3XY6oqfdzn913vUcjCSnpvCKpfx",label:"better error codes for tx lamport check"},{id:"AnAP9zPV4KL7czAPQbFhpDKV2tx7g4UGNbK9wvXwjaRo",label:"bls pubkey management in vote account"},{id:"9gxu85LYRAcZL38We8MYJ4A9AwgBBPtVBAqebMcT1241",label:"cap accounts data allocations per transaction"},{id:"9k5ijzTbYPtjzu8wj2ErH9v45xecHzQ1x4PMYMMxFgdM",label:"cap bpf program instruction accounts"},{id:"DdLwVYuvDz26JohmgSbA7mjpJFgX5zP2dkp8qsF2C33V",label:"cap transaction accounts data size"},{id:"3ccR6QpxGYsAbWyfevEtBNGfWV4xBffxRj2tD6A9i39F",label:"check init vote data"},{id:"nWBqjr3gpETbiaVj3CBJ3HFC5TMdnJDGt21hnvSTvVZ",label:"check physical overlapping"},{id:"GmC19j9qLn2RFk5NduX6QXaDhVpGncVVBzyM8e9WMz2F",label:"check slice translation size"},{id:"3uRVPBpyEJRo1emLCrq38eLRFGcu6uKSpUXqGvU8T7SZ",label:"check syscall outputs do not overlap"},{id:"5Pecy6ie6XGm22pc9d4P9W5c31BugcFBuy6hsP2zkETv",label:"checked arithmetic in fee validation"},{id:"Bj2jmUsM2iRhfdLLDSTkhM5UQRQvQHm57HSmPibPtEyu",label:"clean up delegation errors"},{id:"Eg7tXEwMZzS98xaZ1YHUbdRHsaYZiCsSaR6sKgxreoaj",label:"commission rate in basis points"},{id:"noRuG2kzACwgaY7TVmLRnUNPLKNVQE1fb7X55YWBehp",label:"commission updates only allowed in first half of epoch"},{id:"86HpNqzutEZwLcPxS6EHDcMNYWk6ikhteg9un7Y2PBKE",label:"compact vote state updates"},{id:"6YsBCejwK96GZCkJ6mkZ4b68oP63z2PLoQmWjC7ggTqZ",label:"consume blockstore duplicate proofs"},{id:"wLckV1a64ngtcKPRGU4S4grVTestXjmNjxBjaKZrAcn",label:"cost model requested write lock cost"},{id:"6sPDzwyARRExKH52LECxcGoqziH8G7SZofwuxi8Ja331",label:"create account allow prefund"},{id:"BUS12ciZ5gCoFafUHWW8qaFMMtwFQGVxjsDheWLdqBE2",label:"credits auto rewind"},{id:"eca6zf6JJRjQsYYPkBHF3N32MTzur4n2WL4QiiacPCL",label:"curve25519 restrict msm length"},{id:"7rcw5UtqgDTBBv2EcynNfYckgdAaH1MAsCjKgXMkN7Ri",label:"curve25519 syscall enabled"},{id:"8kEuAshXLsgkUEdcFVLqrjCGGHVWFW99ZZpxvAzzMtBp",label:"dedupe config program signers"},{id:"J2QdYx8crLbTVK8nur1jeLsmc3krDbfjoxoea2V1Uy5Q",label:"default units per instruction"},{id:"LTDSzjZKFJMKHYpNycG1FrWwGGTaFFwqEFjB5GGLNVD",label:"define ltds fee only semantics"},{id:"76dHtohc2s5dR3ahJyBxs7eJJVipFkaPdih9CLgTTb4B",label:"delay commission updates"},{id:"GmuBvtFb2aHfSfMXpuFeWZGHyDeCLPS79s48fmCWCfM5",label:"delay visibility of program deployment"},{id:"3E3jV7v9VcdJL8iYZUMax9DiDno8j7EWUVbhm9RtShj2",label:"demote program write locks"},{id:"B7H2caeia4ZFcpE3QcgMqbiWiBtWrdBRBSJ1DY6Ktxbq",label:"deplete cu meter on vm failure"},{id:"rent6iVy6PDoViPBeJ6k5EJQrkj62h7DPyLbWGHwjrC",label:"deprecate rent exemption threshold"},{id:"GaBtBJvmS4Arjj5W1NmFcyvPjsHN38UGYDq2MDwbs9Qu",label:"deprecate rewards sysvar"},{id:"6Uf8S75PVh91MYgPQSHnjRAPQq6an5BDv9vomrCwDqLe",label:"deprecate unused legacy vote plumbing"},{id:"EQUMpNFr7Nacb1sva56xn1aLfBxppEoSBH8RRVdkcD1x",label:"disable account loader special case"},{id:"3XgNukcZWf9o3HdA3fpJbm94XFc4qpvTXc8h1wxYwiPi",label:"disable bpf deprecated load instructions"},{id:"7WeS1vfPRgeeoXArLh7879YcB9mgE9ktjPDtajXeWfXn",label:"disable bpf loader instructions"},{id:"4yuaYAj2jGMGTh1sSmi4G2eFscsDq8qjugJXZoBN6YEa",label:"disable bpf unresolved symbols at runtime"},{id:"4UDcAfQ6EcA6bdcadkeHpkarkhZGJ7Bpq7wTAiRMjkoi",label:"disable builtin loader ownership chains"},{id:"B9cdB55u4jQsDNsdTK525yE9dmSc5Ga7YBaBrDFvEhM9",label:"disable cpi setting executable and rent epoch"},{id:"79HWsX9rpnnJBPcdNURVqygpMAfxdrAirzAGAVmf92im",label:"disable deploy of alloc free syscall"},{id:"GTUMCZ8LTNxVfxdrw7ZsDFTxXb7TutYkzJnFwinpE6dg",label:"disable deprecated loader"},{id:"2jXx2yDmGysmBKfKYNgLj2DQyAQv6mMk2BPh4eSbyB4H",label:"disable fee calculator"},{id:"JAN1trEUEtZjgXYzNBYHU9DYd7GnThhXfFP7SzPXkPsG",label:"disable fees sysvar"},{id:"2B2SBNbUcr438LtGXNcJNBP2GBSxjx81F945SdSkUSfC",label:"disable partitioned rent collection"},{id:"DTVTkmw3JSofd8CJVJte8PXEbxNQ2yZijvVr3pe2APPj",label:"disable rehash for rent epoch"},{id:"CJzY83ggJHqPGDq8VisV3U91jDJLuEaALZooBrXtnnLU",label:"disable rent fees collection"},{id:"turbnbNRp22nwZCmgVVXFSshz7H7V23zMzQgA46YpmQ",label:"disable turbine fanout experiments"},{id:"zkdoVwnSFnSLtGJG7irJPEYUpmb4i7sGMGcnN6T9rnC",label:"disable zk elgamal proof program"},{id:"disCA4efguFL6Wqa4pGdG7jpjC7C5uiKzKnhEBqchBe",label:"discard unexpected data complete shreds"},{id:"75m6ysz33AfLA5DDEzWM1obBrnPQRSsdVQ2nRmc8Vuu1",label:"do support realloc"},{id:"GV49KKQdBNaiv2pgqhS2Dy3GWYJGXMTVYbYkdk91orRy",label:"drop legacy shreds"},{id:"4Di3y24QFLt5QEUPZtbnjyfQKfm6ZMTfa6Dw1psfoMKU",label:"drop redundant turbine path"},{id:"5KLGJSASDVxKPjLCDWNtnABLpZjsQSrYZ8HKwcEdAMC8",label:"drop unchained merkle shreds"},{id:"ed9tNscbWLYBooxWA7FE2B5KHWs8A6sxfY8EzezEcoo",label:"ed25519 precompile verify strict"},{id:"6ppMXNYLhVd7GcsZ5uV11wQEW7spppiMVfqQv5SXhDpX",label:"ed25519 program enabled"},{id:"EJJewYSddEEtSZHiqugnvhQHiWyZKjkFDQASd7oKSagn",label:"enable alt bn128 compression syscall"},{id:"bn1hKNURMGQaQoEVxahcEAcqiX3NwRs6hgKKNSLeKxH",label:"enable alt bn128 g2 syscalls"},{id:"A16q37opZdQMCbe5qJ6xpBB9usykfv8jZaMkxvZQi4GJ",label:"enable alt bn128 syscall"},{id:"b1sgUiJ3qu7hYm3tNDyyqZNQd6gLGJmJppnLNa93PCQ",label:"enable bls12 381 syscall"},{id:"8Zs9W7D9MpSEtUWSQdGniZk2cNmV22y6FLJwCx53asme",label:"enable bpf loader extend program ix"},{id:"5x3825XS7M2A3Ekbn5VGGkvFoAg5qrRWkTrY4bARP1GL",label:"enable bpf loader set authority checked ix"},{id:"7uZBkJXJ1HkuP6R3MJfZs7mLwymBcDbKdqbF51ZWLier",label:"enable chained merkle shreds"},{id:"4EJQtF2pkRyawwcTVfQutzq4Sa5hRhibF6QAK1QXhtEX",label:"enable durable nonce"},{id:"7Vced912WrRnfjaiKRiNBcbuFw7RrnLv3E3z95Y4GTNc",label:"enable early verification of account modifications"},{id:"FKe75t4LXxGaQnVHdUKM6DSFifVVraGZ8LyNo7oPwy1Z",label:"enable get epoch stake syscall"},{id:"FNKCMBzYUdjhHyPdsKG2LSmdzH8TCHXn3ytj8RNBS4nG",label:"enable gossip duplicate proof ingestion"},{id:"FL9RsQA6TVUoh5xJQ9d936RHSebA1NLQqe3Zv9sXZRpr",label:"enable poseidon syscall"},{id:"J4HFT8usBxpcF63y46t1upYobJgChmKyZPm5uTBRg25Z",label:"enable program redeployment cooldown"},{id:"Hr1nUA9b7NJ6eChS26o7Vi8gYYDDwWD3YeBfzJkTbU86",label:"enable request heap frame ix"},{id:"JE86WkYvTrzW8HgNmrHY7dFYpCmSptUpKupbo2AdQ9cG",label:"enable sbpf v1 deployment and execution"},{id:"F6UVKh1ujTEFK3en2SyAL3cdVnqko1FVEXWhmdLRu6WP",label:"enable sbpf v2 deployment and execution"},{id:"5cC3foj77CWun58pC51ebHFUWavHWKarWyR5UUik7dnC",label:"enable sbpf v3 deployment and execution"},{id:"srremy31J5Y25FrAApwVb9kZcfXbusYMMsvTK9aWv5q",label:"enable secp256r1 precompile"},{id:"tSynMCspg4xFiCj1v3TDb4c7crMR5tSBhLz4sF7rrNA",label:"enable tower sync ix"},{id:"PaymEPK2oqwT9TXAVfadjztH2H6KfLEB9Hhd5Q5frvP",label:"enable transaction loading failure fees"},{id:"D31EFnLgdiysi84Woo3of4JMu7VmasUS3Z7j9HYXCeLY",label:"enable turbine fanout experiments"},{id:"5JsG4NWH8Jbrqdd8uL6BNwnyZK3dQSoieRXG5vmofj9y",label:"enable vote address leader schedule"},{id:"fixfecLZYMfkGzwq6NJA11Yw6KYztzXiK9QcL3K78in",label:"enforce fixed fec set"},{id:"5GpmAKxaGsWWbPp4bNXFLJxZVvG92ctxf7jQnzTQjF3n",label:"epoch accounts hash"},{id:"8199Q2gMD2kwgfopK5qqVWuDbegLgpuFUFHCcUJQDN8b",label:"error on syscall bpf function hash collisions"},{id:"EMX9Q7TVFAmQ9V1CggAkhMzhXSg8ECp7fHrWQX2G1chf",label:"evict invalid stakes cache entries"},{id:"7GUcYgq4tVtaqNCKT3dho9r4665Qp5TxCZ27Qgjx3829",label:"executables incur cpi data cost"},{id:"GE7fRxmW46K6EmCD9AMZSbnaJ2e3LfqCZzdHi9hmYAgi",label:"filter stake delegation accounts"},{id:"3gtZPqvPpsbXZVCx6hceMfWxtsmrjMzmg8C7PLKSxS2d",label:"filter votes outside slot hashes"},{id:"bn2puAyxUx6JUabAxYdKdJ5QHbNNmKw8dCGuGCyRrFN",label:"fix alt bn128 multiplication input length"},{id:"bnYzodLwmybj7e1HAe98yZrdJTd7we69eMMLgCXqKZm",label:"fix alt bn128 pairing length check"},{id:"6iyggb5MTcsvdcugX7bEKbHV8c6jdLbpHwkncrgLMhfo",label:"fix recent blockhashes"},{id:"36PRUK2Dz6HWYdG9SpjeAsF5F3KxnFCakA2BZMbtMhSb",label:"fixed memcpy nonoverlapping check"},{id:"DeS7sR48ZcFTUmt5FFEVDr1v1bh73aAbZiZq3SYr8Eh8",label:"formalize loaded transaction data size"},{id:"7XRJcS5Ud5vxGB54JbK9N2vBZVwnwdBNeJW1ibRgD9gx",label:"full inflation \u00b7 mainnet \u00b7 certusone \u00b7 enable"},{id:"CLCoTADvV64PSrnR6QXty6Fwrt9Xc6EdxSJE4wLRePjq",label:"get sysvar syscall enabled"},{id:"2R72wpcQ7qV7aTJWUumdn8u5wmmTyXbK7qzEy7YSAgyY",label:"include account index in rent error"},{id:"H6iVbVaDZgDphcPbcZwc5LoznMPWQfnJ1AM7L1xzqvt5",label:"increase cpi account info limit"},{id:"25vqsfjk7Nv1prsQJmA4Xu1bN61s8LXCBGUPp8Rfy1UF",label:"incremental snapshot only incremental hash calculation"},{id:"dupPajaLy2SSn8ko42aZz4mHANDNrLe8Nw8VQgFecLa",label:"index erasure conflict duplicate proofs"},{id:"H3kBSaKdeiUsyHmeHqjJYNc27jesXZ6zWj3zWkowQbkV",label:"instructions sysvar owned by sysvar"},{id:"HooKD5NC9QNxk25QuzCssB8ecrEzGt6eXEPBUxWp1LaR",label:"last restart slot sysvar"},{id:"E8MkiWZNNPGU6n55jkGzyj8ghUmjCHRmDFdYYFYHxWhQ",label:"leave nonce on success"},{id:"DhsYfRjxfnh2g7HKJYSzT79r74Afa1wbHkAgHndrA1oy",label:"libsecp256k1 0 5 upgrade enabled"},{id:"54KAoNiUERNoWWUhTWWwXgym94gzoXFVnHyQwPA18V9A",label:"libsecp256k1 fail on bad count2"},{id:"6aHuNsUmwSzCEMjrBzBCYaxHAyAcQBjVES92JigHBDuC",label:"limit instruction accounts"},{id:"GQALDaC48fEhZGWRj9iL5Q889emJKcj3aCvHF7VCbbF4",label:"limit max instruction trace length"},{id:"7g9EUwj4j7CS21Yx1wvgWLjSZeh5aPq8x9kpoPwXM8n8",label:"limit secp256k1 recovery id"},{id:"YbbRLkvenrocjGPGyoQE4wjnvYzTgfsk38NFmcYK7a5",label:"loader v3 minimum extend program size"},{id:"GDH5TVdbTPUpRnXaRyQqiKUa7uZAbZ28Q2N9bhbKoMLm",label:"loosen cpi size restriction"},{id:"RENtePQcDLrAbxAsP3k8dwVcnNYQ466hi2uKvALjnXx",label:"mask out rent epoch in vm serialization"},{id:"CBkDroRDqm8HwHe6ak9cguPjUomrASEkfmxEaZ5CNNxz",label:"max tx account locks"},{id:"21AWDosvp3pBamFW91KB35pNoaoZVTM7ess8nr2nt53B",label:"merge nonce error into system error"},{id:"mrkPjRg79B2oK2ZLgd7S3AfEJaX9B6gAF3H9aEykRUS",label:"merkle conflict duplicate proofs"},{id:"C97eKZygrkU4JxJsZdjgbUY7iQR7rKTr4NyDWo2E5pRm",label:"migrate address lookup table program to core bpf"},{id:"2Fr57nzzkLYXW695UdDxDeR5fhnZWSttZeZYemrnpGFV",label:"migrate config program to core bpf"},{id:"4eohviozzEeivk1y9UbrnekbAFMDQyJz5JjA9Y6gyvky",label:"migrate feature gate program to core bpf"},{id:"6M4oQ6eXneVhtLoiAr4yRYQY43eVLjrKbiDZDJc892yk",label:"migrate stake program to core bpf"},{id:"9ypxGLzkMxi89eDerRKXWDXe44UY2z4hBig4mDhNq5Dp",label:"move precompile verification to svm"},{id:"74CoWuBmt3rUVUrCb2JiSTvh6nXyBWUsK4SaMj3CtE3T",label:"move serialized len ptr in cpi"},{id:"7bTK6Jis8Xpfrs8ZoUfiMDPazTcdPcTWheZFJTA5Z6X4",label:"move stake and move lamports ixs"},{id:"8pgXCMNXC8qyEFypuwpXyRxLXZdpM4Qo72gJ6k87A6wL",label:"native programs consume cu"},{id:"4kpdyrcj5jS47CZb2oJGfVxjYbsMm2Kx97gFyZrxxwXz",label:"no overflow rent distribution"},{id:"3u3Er5Vc2jVcwz4xr2GJeSAXT3fAj6ADHZ4BJMZiScFd",label:"nonce must be advanceable"},{id:"HxrEu1gXuH7iD3Puua1ohd5n4iUKJyFNtNxk9DVJkvgr",label:"nonce must be authorized"},{id:"BiCU7M5w8ZCMykVSyhZ7Q3m2SWoR2qrEQ86ERcDX77ME",label:"nonce must be writable"},{id:"CpkdQmspsaZZ8FVAouQTtTWZkc8eeQ7V3uj7dWz543rZ",label:"on load preserve rent epoch for rent exempt accounts"},{id:"265hPS8k8xJ37ot82KEgjRunsUp5w4n4Q4VwwiN9i9ps",label:"optimize epoch boundary updates"},{id:"PERzQrt5gBD1XEe2c9XdFWqwgHY3mr7cYWbm5V772V8",label:"partitioned epoch rewards superfeature"},{id:"4RWNif6C2WCNiKVW7otP4G7dkmkHGyKQWRpuZ1pxKU5m",label:"pico inflation"},{id:"poUdAqRXXsNmfqAZ6UqpjbeYgwBygbfQLEvWSqVhSnb",label:"poseidon enforce padding"},{id:"HH3MUYReL2BvqqA3oEcAa7txju5GY6G4nxJ51zvsEjEZ",label:"preserve rent epoch for rent exempt accounts"},{id:"4ApgRX3ud6p7LNMJmsuaAcZY5HWctGPr5obAsjB3A54d",label:"prevent calling precompiles as programs"},{id:"812kqX67odAp5NFwM8D2N24cku7WTm9CHUTFUXaDkWPn",label:"prevent crediting accounts that end rent paying"},{id:"Fab5oP3DmsLYCiQZXdjyqT3ukFFPrsmqhXU4WU1AWVVF",label:"prevent rent paying rent recipients"},{id:"5xXZc66h4UdB6Yq7FzdBxBiRAFMMScMLwHxk2QZDaNZL",label:"provide instruction data offset in vm r2"},{id:"DpJREPyuMZ5nDfU6H3WTqSqUFSXAfw8u7xqmWtEwJDcP",label:"quick bail on panic"},{id:"htsptAwi2yRoZH83SKaUXykeZGtZHgxkS2QwW1pssR8",label:"raise account cu limit"},{id:"P1BCUMpAC7V2GRBRiJCNUgpMyWZhoqt3LKo712ePqsz",label:"raise block limits to 100m"},{id:"5oMCU3JPaFLr8Zr4ct7yFA7jdk6Mw1RmB8K4u9ZbS42z",label:"raise block limits to 50m"},{id:"6oMCUgfY6BzZ6jwB681J6ju5Bh6CjVXbd7NeWYqiXBSu",label:"raise block limits to 60m"},{id:"3aJdcZqxoLpSBxgeYGjPwaYS1zzcByxUDqJkbzWAH1Zb",label:"record instruction in transaction context push"},{id:"EBeznQDjcPG8491sFsKZYBi5S5jTVXMpAKNDJMQPS2kq",label:"reduce required deploy balance"},{id:"GwtDQBghCTBgmX2cpEGNPxTEBUTQRaDMGTr5qychdGMj",label:"reduce stake warmup cooldown"},{id:"zkexuyPRdyTVbZqEAREueqL2xvvoBhRgth9xGSc1tMN",label:"reenable zk elgamal proof program"},{id:"3NKRSwpySNwD3TvP5pHnRmkAQRsdkXWRr1WaQh8p4PWX",label:"reject callx r10"},{id:"9kdtFSrXHQg3hKkbXkQ6trJ3Ja1xpJ22CTFSNAciEwmL",label:"reject empty instruction without program"},{id:"7txXZZD6Um59YoLMF7XUNimbMjsqsWhc7g2EniiTrmp1",label:"reject non rent exempt vote withdraws"},{id:"ALBk3EWdeAg2WAGf6GPDUf1nynyNqCdEVmgouG7rpuCj",label:"reject vote account close unless zero credit epoch"},{id:"FKAcEvNgSY79RpqsPNUV5gDyumopH4cEHqUxyfm8b8Ap",label:"relax authority signer check for lookup table creation"},{id:"4WeHX6QoXCCwqbSFgi6dxnB6QsPo6YApaNTH7P4MLQ99",label:"relax intrabatch account locks"},{id:"rexav5eNTUSNT1K2N7cfRjnthwhcP5BC25v2tA4rW4h",label:"relax programdata account check migration"},{id:"LTdLt9Ycbyoipz5fLysCi1NnDnASsZfmJLJXts5ZxZz",label:"remove accounts delta hash"},{id:"FXs1zh47QbNnhXcnB6YiAQoJ4sGB91tKF3UFHLcKT7PM",label:"remove accounts executable flag checks"},{id:"2HmTkCj9tXuPE4ueHzdD7jPeMf9JGCoZh5AsyoATiWEe",label:"remove bpf loader incorrect program id"},{id:"A8xyMHZovGXFkorFqEmVH2PKGLiBip5JD7jt4zsUWo4H",label:"remove congestion multiplier from fee calculation"},{id:"EfhYd3SafzGT472tYQDUc4dPd2xdEfKs5fwkowUgVt4W",label:"remove deprecated request unit ix"},{id:"HTTgmruMYRZEntyL3EdCDdnS6e4D5wRq1FA7kQsb66qq",label:"remove native loader"},{id:"BtVN7YjDzNE6Dk7kTT7YTDgMNUZTNgiSJgsdzAeTg2jF",label:"remove rounding in fee calculation"},{id:"2GCrNXbzmt4xrwdcKS2RdsLzsgu4V5zHAemW57pcHT6a",label:"remove simple vote from cost model"},{id:"BKCPBQQBZqggVnFso5nQ8rQ4RwwogYwjuUt9biBjxwNF",label:"rent for sysvars"},{id:"ptokFjwyJtrwCa9Kgo9xoDS59V4QccBGEaRFnRPnSdP",label:"replace spl token with p token"},{id:"CCu4boMmfLuqcmfTLPHQiUo22ZdUsXjgzPAURYaWt1Bw",label:"requestable heap size"},{id:"D4jsDcXaqdW8tDAWn8H4R25Cdns2YwLneujSL1zvjW6R",label:"require custodian for locked stake authorize"},{id:"BkFDxiJQWZXGTZaJQxH7wVEHkAmwCgSEVkrvswFfRJPD",label:"require rent exempt accounts"},{id:"D2aip4BBr8NPWtU9vLrwrBvbuaQ8w1zV38zFLxx4pfBV",label:"require rent exempt split destination"},{id:"7VVhpg5oAjAmnmz1zCcSHb2Z9ecZB2FQqpnEwReka9Zm",label:"require static nonce account"},{id:"8FdwgyHFEjhAdjWfV2vfqk7wA1g9X3fQpKH7SBpEv3kC",label:"require static program ids in transaction"},{id:"C9oAhLxDBm3ssWtJx1yBGzPY55r2rArHmN1pbQn6HogH",label:"reserve minimal cus for builtin instructions"},{id:"DwScAzPUjuv65TMbDnFY7AgwmotzWy3xpEJMXM3hZFaB",label:"return data syscall enabled"},{id:"BTWmtJC8U5ZLMbBUUA1k6As62sYjPEjAiNAT55xYGdJU",label:"revise turbine epoch stakes"},{id:"3opE3EzAKnUftUDURkzMgwpNgimBAypW1mNDYH4x4Zg7",label:"reward full priority fee"},{id:"CE2et8pqgyQMP2mQRg3CgvX8nJBKUArMu3wfiQiQKY1y",label:"round up heap size"},{id:"E3PHP7w8kB7np3CTQ1qQ2tW3KCtjRSXBQgW9vM2mWv2Y",label:"secp256k1 program enabled"},{id:"6RvdSWHh8oh72Dp7wMTS2DBkf3fRPtChfNrAo3cZZoXJ",label:"secp256k1 recover syscall enabled"},{id:"C5fh68nJ7uyKAuYZg2x9sEQ5YrVf3dkW6oojNBSc3Jvo",label:"send to tpu vote port"},{id:"Gea3ZkK2N4pHuVZVxWcnAtS6UEDdyumdYt4pFcKjA3ar",label:"separate nonce from blockhash"},{id:"5wAGiy15X1Jb2hkHnPDCM8oB9V42VNA9ftNVFK84dEgv",label:"set exempt rent epoch max"},{id:"JDn5q3GBeqzvUa7z67BbmVHVdE3EbUAjvFep3weR3jxX",label:"simplify alt bn128 syscall error codes"},{id:"5ZCcFAzJ1zsFKe1KSZa9K92jhx7gkcKj97ci2DBo1vwj",label:"simplify writable program account check"},{id:"CGB2jM8pwZkeeiXQ66kBMyBR6Np61mggL7XUsmLjVcrw",label:"skip rent rewrites"},{id:"LTsNAP8h1voEVVToMNBNqoiNQex4aqfUrbFhRH3mSQ2",label:"snapshots lt hash"},{id:"6uaHcKPGUy4J7emLBgUTeufhJdiwhngW6a1R9B7c2ob9",label:"sol log data syscall enabled"},{id:"FaTa4SpiaSNH44PGC4z8bnGVTkSRYaWvrBs3KTu8XQQq",label:"spl associated token account v1 0 4"},{id:"FaTa17gVKoqbh38HcfiQonPsAaQViyDCCSg71AubYZw8",label:"spl associated token account v1 1 0"},{id:"E5JiFDQCwyC6QfT9REFyMpfK2mHcmv1GUDySU1Ue7TYv",label:"spl token v2 multisig fix"},{id:"BL99GYhdjjcv6ys22C9wPgn2aTVERDbPHHo4NbS3hgp7",label:"spl token v2 self transfer fix"},{id:"FToKNBYyiF4ky9s8WsmLBXHCht17Ek7RXaLZGHzzQhJ1",label:"spl token v2 set authority fix"},{id:"Ftok2jhqAqxUWEiCVRrfRs9DPppWP8cgTB7NQNKL88mS",label:"spl token v3 3 0 release"},{id:"Ftok4njE8b7tDffYkC5bAbCaQv5sL6jispYrprzatUwN",label:"spl token v3 4 0"},{id:"sTKz343FM8mqtyGvYWvbLpTThw3ixRM4Xk8QvZ985mw",label:"stake allow zero undelegated amount"},{id:"437r62HoAdUb63amq3D7ENnBLDhHT2xY8eFkLJYVKK4x",label:"stake deactivate delinquent instruction"},{id:"meRgp4ArRPhD3KtCY9c5yAf2med7mBLsjKTPeVUHqBL",label:"stake merge with unmatched credits observed"},{id:"SAdVFw3RZvzbo6DvySbSdBnHN4gkzSTH9dSxesyKKPj",label:"stake program advance activating credits observed"},{id:"FQnc7U4koHqWgRvFaBJjZnV8VPg6L6wWK33yJeDp4yvV",label:"stake split uses rent sysvar"},{id:"HFpdDDNQjvcXnXKec697HDDsyk6tFoWS2o8fkxuhQZpL",label:"stakes remove delegation if inactive"},{id:"64ixypL1HPu8WtJhNSMb9mSgfFaJvsANuRkTbHyuLfnx",label:"static instruction limit"},{id:"EYVpEP7uzH1CoXzbD6PubGhYmnxRXPeq3PPsm1ba3gpo",label:"stop sibling instruction search at parent"},{id:"16FMCmgLzCNNz6eTwGanbyN2ZxvTBSLuQ6DZhgeMshg",label:"stop truncating strings in syscalls"},{id:"CHaChatUnR3s6cPyPMMGNJa3VdQQ8PNH2JqdD4LpCKnB",label:"switch to chacha8 turbine"},{id:"Cdkc8PPTeTNUPoZEfCY5AyetUrEdkZtNPMgz58nqyaHD",label:"switch to new elf parser"},{id:"EDGMC5kxFxGk4ixsNkGt8bW7QL5hDMXnbwaZvYMwNfzF",label:"syscall parameter address restrictions"},{id:"HyrbKftCdJ5CrUfEti6x26Cj7rZLNe32weugk7tLcWb8",label:"syscall saturated math"},{id:"BrTR9hzw4WBGFP65AJMbpAo64DcA3U6jdPSga9fMV5cS",label:"system transfer zero check"},{id:"tvcF6b1TRz353zKuhBjinZkKzjmihXmBAHJdjNYw1sQ",label:"timely vote credits"},{id:"5ekBxc8itEnPv4NzGJtr8BVVQLNMQuLMNQQj7pHoLNZ9",label:"tx wide compute cap"},{id:"3uFHb9oKdGfgZGJK9EHaAXN4USvnQtAFC13Fh5gGFS5B",label:"update hashes per tick"},{id:"EWme9uFqfy1ikK1jhJs8fM5hxWnK336QJpbscNtizkTU",label:"update hashes per tick2"},{id:"8C8MCtsab5SsfammbzvYz65HHauuUYdbY2DZ4sznH6h5",label:"update hashes per tick3"},{id:"8We4E7DPwF2WfAN8tRTtWQNhi98B99Qpuj7JoZ3Aikgg",label:"update hashes per tick4"},{id:"BsKLKAn1WM4HVhPRDsjosmqSg2J8Tq5xP2s2daDS6Ni4",label:"update hashes per tick5"},{id:"FKu1qYwLQSiehz644H6Si65U5ZQ2cp9GxsyFUfYcuADv",label:"update hashes per tick6"},{id:"28s7i3htzhahXQKqmS2ExzbEoUypg9krwvtK2M9UWXh9",label:"update rewards from cached accounts"},{id:"2h63t332mGCCsWK2nqqqHhN4U9ayyqhLVFvczznHDoTZ",label:"update syscall base costs"},{id:"STk5Xj8hdAx3sTzmtJ3QysKkq6X2A3yj73JtxttiRyk",label:"upgrade bpf stake program to v5"},{id:"8sKQrMQoUHtQSUP83SPG4ta2JDjSAiWs7t5aJ9uEd6To",label:"use default units in fee calculation"},{id:"vcmrbYbiMVKaq1snKP6eCacNDcr6qZvpCNUjmk6gxvZ",label:"validate chained block id"},{id:"vcmrw431aNM8ngQ46derkZXipoTGQdbHkEygBDh12dA",label:"validate chained block id 2"},{id:"prpFrMtgNmzaNzkPJg9o753fVvbHKqNrNTm76foJ2wm",label:"validate fee collector account"},{id:"VAT9huvhPjRN9cyrPytq9rwvEJ3J4ADtjdncgZRyANJ",label:"validator admission ticket"},{id:"EVW9B5xD9FFK7vw1SBARwMA4s5eRo5eKJdKpsBikzKBz",label:"verify tx signatures len"},{id:"3KZZ6Ks1885aGBQ45fwRcPXVBCtzUvxhUTkwKMR41Tca",label:"versioned tx message enabled"},{id:"6tRxEYKuy2L5nnv5bgn7iT28MxUbYxp5h7F3Ncf1exrT",label:"vote authorize with seed"},{id:"ffecLRhhakKSGhMuc6Fz2Lnfq4uT9q3iu9ZsNaPLxPc",label:"vote only full fec sets"},{id:"BcWknVcgvonN8sL4HE4XFuEVgfcee5MwxWPAgP6ZV89X",label:"vote stake checked instructions"},{id:"7axKe5BTYBDD87ftzWbk5DfzWMGyRvqmWTduuo22Yaqy",label:"vote state add vote latency"},{id:"CveezY6FDLVBToHDcvJRmtMouqzsmj4UXYh5ths5G5Uv",label:"vote state update credit per dequeue"},{id:"G74BkWBzmsByZ1kxHy44H3wjwp5hp7JbrGRuDpco22tY",label:"vote state update root fix"},{id:"Gx4XFcrVMt4HUvPzTpTSVkdDVgcDSjKhDN1RqRS6KDuZ",label:"vote state v4"},{id:"AVZS3ZsN4gi6Rkx2QUibYuSJG3S6QHib7xCYhG6vGJxU",label:"vote withdraw authority may change authorized voter"},{id:"GvDsGDkH5gyzwpDhxNixx8vtx1kwYHH13RiNAPw27zXb",label:"warp timestamp again"},{id:"3BX6SBeEBibHaVQXywdkcgyUk6evfYZkHdztXiDtEpFS",label:"warp timestamp with a vengeance"},{id:"zkhiy5oLowR7HY4zogXjCjeMXyruLqBwSWH21qcFtnv",label:"zk elgamal proof program enabled"}];
const FEATURE_GATES = [
{ id: 'CLCoTADvV64PSrnR6QXty6Fwrt9Xc6EdxSJE4wLRePjq', name: 'sol_get_sysvar syscall', note: 'cheap sysvar reads; many programs depend on it' },
{ id: '6TkHkRmP7JZy1fdM6fg5uXn76wChQBWGokHBJzrLB3mj', name: 'raise CPI nesting limit to 8', note: 'deeper cross-program call chains' },
{ id: 'A16q37opZdQMCbe5qJ6xpBB9usykfv8jZaMkxvZQi4GJ', name: 'alt_bn128 syscalls', note: 'EVM-compatible pairing / bn254 math' },
{ id: '7rcw5UtqgDTBBv2EcynNfYckgdAaH1MAsCjKgXMkN7Ri', name: 'curve25519 syscalls', note: 'ed25519 / ristretto ops in-program' },
{ id: 'zkhiy5oLowR7HY4zogXjCjeMXyruLqBwSWH21qcFtnv', name: 'ZK ElGamal proof program', note: 'confidential-transfer proofs' },
{ id: '5x3825XS7M2A3Ekbn5VGGkvFoAg5qrRWkTrY4bARP1GL', name: 'loader SetAuthorityChecked ix', note: 'safer program upgrade-authority change' },
];
// Notable *pending* gates — not yet active on mainnet. These are OFF by default;
// toggle one ON to activate it early and test "does my program survive this?".
// direct mapping (virtual_address_space_adjustments) changes how account memory is
// mapped into the VM — the runtime honors it, and a program with an unsafe write to
// a readonly account will start failing once it's live.
const PENDING_GATES = [
{ id: '7VgiehxNxu53KdxgLspGQY8myE6f7UokaWa4jsGcaSz', name: 'direct mapping', note: 'account data mapped straight into the VM — activate to check your program still passes' },
];
const isPending = (id) => PENDING_GATES.some(g => g.id === id);
// Sent as `features`; empty = mainnet as-is. Only holds gates flipped off default.
let activeFeatures = [];
const featState = (id) => activeFeatures.find(f => f.id === id);
const gateLabel = (id) => (ALL_GATES.find(g => g.id === id) || {}).label;
function gateRow(id, label, note, custom) {
const off = featState(id)?.active === false;
const isCustom = custom || (featState(id)?.active === true);
const on = isCustom ? (featState(id)?.active === true) : !off;
return `<label class="feat-row${on ? '' : ' off'}">
<span class="feat-main"><span class="feat-name">${esc(label)}</span>
${note ? `<span class="feat-note">${esc(note)}</span>` : ''}</span>
<span class="switch"><input type="checkbox" data-fid="${id}" ${on ? 'checked' : ''}${isCustom ? ' data-custom="1"' : ''} /><span class="slider"></span></span></label>`;
}
function renderFeatList() {
const q = ($('featSearch')?.value || '').trim().toLowerCase();
let html = '';
if (!q) {
// No search: curated "notable" active gates, then notable pending activations.
html += `<div class="feat-head">Notable</div>`;
html += FEATURE_GATES.map(g => gateRow(g.id, g.name, g.note)).join('');
html += `<div class="feat-head">Pending activation · not yet on mainnet</div>`;
html += PENDING_GATES.map(g => gateRow(g.id, g.name, g.note, true)).join('');
// Plus any custom pending gates the user added by pubkey.
const known = new Set([...ALL_GATES, ...FEATURE_GATES, ...PENDING_GATES].map(g => g.id));
const customs = activeFeatures.filter(f => !known.has(f.id));
if (customs.length) html += `<div class="feat-head">Custom</div>` +
customs.map(f => gateRow(f.id, short(f.id), 'custom gate', true)).join('');
} else {
// Searching: mainnet-active gates + notable pending ones.
const pend = PENDING_GATES.filter(g => g.name.toLowerCase().includes(q) || g.id.toLowerCase().includes(q));
const hits = ALL_GATES.filter(g => g.label.toLowerCase().includes(q) || g.id.toLowerCase().includes(q)).slice(0, 60);
html += `<div class="feat-head">${hits.length + pend.length} match${hits.length + pend.length === 1 ? '' : 'es'}</div>`;
html += pend.map(g => gateRow(g.id, g.name + ' · pending', g.note, true)).join('');
html += hits.map(g => gateRow(g.id, g.label)).join('');
if (!hits.length && !pend.length) html += `<div class="feat-note" style="padding:8px 10px">no gate matches “${esc(q)}”</div>`;
}
$('featList').innerHTML = html;
$('featList').querySelectorAll('input[data-fid]').forEach(cb =>
cb.addEventListener('change', () => toggleFeature(cb.dataset.fid, cb.checked, cb.dataset.custom === '1')));
}
function toggleFeature(id, checked, isCustom) {
activeFeatures = activeFeatures.filter(f => f.id !== id);
// Curated gates are on by default: only record when turned OFF.
// Custom gates were added to activate: recording keeps them, unchecking drops them.
if (isCustom) { if (checked) activeFeatures.push({ id, active: true }); }
else if (!checked) activeFeatures.push({ id, active: false });
refreshFeatures();
}
function refreshFeatures() {
const n = activeFeatures.length;
$('featLabel').textContent = n ? `⚑ ${n} feature${n === 1 ? '' : 's'}` : '⚑ Features';
$('featBtn').classList.toggle('warped', !!n);
$('featState').textContent = n
? activeFeatures.map(f => `${short(f.id)} ${f.active ? 'on' : 'off'}`).join(' · ')
: "Running under mainnet's active feature set.";
const b = $('featBanner');
if (b) {
b.classList.toggle('hidden', !n);
if (n) b.innerHTML = `<span>⚑</span><span>Replaying with ${n} feature gate${n === 1 ? '' : 's'} flipped: ` +
`${activeFeatures.map(f => esc(short(f.id)) + (f.active ? ' on' : ' off')).join(', ')}. Everything below runs under this feature set.</span>` +
`<button class="rst" id="featReset">reset</button>`;
if (n) $('featReset')?.addEventListener('click', () => { activeFeatures = []; renderFeatList(); refreshFeatures(); onClockChanged(); });
}
renderFeatList();
onClockChanged();
}
$('featBtn').addEventListener('click', e => { e.stopPropagation(); $('featPop').classList.toggle('hidden'); $('clockPop').classList.add('hidden'); });
$('featAdd').addEventListener('click', () => {
const id = ($('featCustom').value || '').trim();
let ok = false; try { ok = bs58decode(id).length === 32; } catch {}
if (!ok) { $('featState').textContent = 'that is not a valid feature gate pubkey'; return; }
activeFeatures = activeFeatures.filter(f => f.id !== id);
activeFeatures.push({ id, active: $('featCustomState').value === 'on' });
$('featCustom').value = '';
refreshFeatures();
});
$('featCustom').addEventListener('keydown', e => { if (e.key === 'Enter') $('featAdd').click(); });
$('featSearch').addEventListener('input', renderFeatList);
renderFeatList();
$('openSim').addEventListener('click', () => showSim(true));
$('navSim').addEventListener('click', () => showSim(true));
$('navAnalyze').addEventListener('click', () => showSim(false));
$('brand').addEventListener('click', () => {
// Logo is always "home" — return to the landing hero regardless of state.
showSim(false);
$('status').classList.add('hidden');
$('out').classList.add('hidden'); $('txlist').classList.add('hidden');
$('hero').classList.remove('hidden');
});
$('closeSim').addEventListener('click', () => showSim(false));
$('runSim').addEventListener('click', runPreflight);
$('loadIx').addEventListener('click', loadInstructions);
$('useIdl').addEventListener('click', loadInstructionsFromIdl);
document.querySelectorAll('.trychip').forEach(chip => chip.addEventListener('click', async () => {
$('cluster').value = chip.dataset.cluster || 'devnet';
$('buildProg').value = chip.dataset.prog;
await loadInstructions();
const want = chip.dataset.ix;
const i = loadedIx.findIndex(x => x.name === want);
if (i >= 0) { $('ixSelect').value = i; renderIxForm(); }
if (chip.dataset.payer) {
const p = $('ixPayer'); p.value = chip.dataset.payer; p.dispatchEvent(new Event('input'));
}
}));
$('ixSelect').addEventListener('change', renderIxForm);
document.querySelectorAll('.tab').forEach(t => t.addEventListener('click', () => {
document.querySelectorAll('.tab').forEach(x => x.classList.toggle('on', x === t));
$('tabPaste').classList.toggle('hidden', t.dataset.tab !== 'paste');
$('tabBuild').classList.toggle('hidden', t.dataset.tab !== 'build');
}));
// ---------- landing theatrics: spotlight, typing terminal, tilt, reveals ----------
const reduceMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;
// Cursor spotlight — lerped so it glides, not snaps.
if (!reduceMotion && matchMedia('(pointer: fine)').matches) {
const spot = $('spot');
let tx = innerWidth / 2, ty = innerHeight * 0.3, x = tx, y = ty;
addEventListener('pointermove', e => { tx = e.clientX; ty = e.clientY; }, { passive: true });
(function glide() {
x += (tx - x) * 0.07; y += (ty - y) * 0.07;
spot.style.transform = `translate(${x - 430}px, ${y - 430}px)`;
requestAnimationFrame(glide);
})();
} else { $('spot').remove(); }
// The terminal types the product's whole pitch on loop:
// replay fails honestly → mutate reality → success → warp time.
const termScript = [
{ cmd: 'svmscope 5Q32pWmaGsNBRU18s…Y8aZtUG9' },
{ out: '<span class="t-br">#3 JUP6…TaV4</span> <span class="t-dim">Jupiter Aggregator v6</span>' },
{ out: '<span class="t-dim"> └─</span> BiSo…Uypi <span class="t-dim">└─</span> Toke…Q5DA' },
{ out: '<span class="t-dim">-- replay --</span> loaded <b>22 accounts</b> + <b>6 program ELFs</b> into the SVM' },
{ out: 'REPLAY: <span class="t-bad">failed ✗</span> Custom(6024) — <span class="t-bad">SlippageExceeded</span>' },
{ pause: 1100 },
{ cmd: 'svmscope 5Q32pWma… --mutate pool.reserve:+2%' },
{ out: 'MUTATED REPLAY: <span class="t-ok">success ✓</span> <span class="t-dim">178,113 CU</span>' },
{ pause: 900 },
{ cmd: 'svmscope test suite.json' },
{ out: '<span class="t-cy">⏱ clock warped +30 days</span> — vesting claim: <span class="t-ok">PASS</span>' },
{ out: '<span class="t-ok">6/6 scenarios passed</span> <span class="t-dim">— deterministic, offline</span>' },
{ pause: 3400 },
];
(async function termLoop() {
const body = $('termBody');
if (!body) return;
const sleep = ms => new Promise(r => setTimeout(r, ms));
const caret = '<span class="caret"></span>';
for (;;) {
body.innerHTML = '';
for (const step of termScript) {
if (step.pause) { await sleep(reduceMotion ? 60 : step.pause); continue; }
const line = document.createElement('div');
line.className = 't-line';
body.appendChild(line);
if (step.cmd) {
for (let i = 0; i <= step.cmd.length; i++) {
line.innerHTML = '<span class="t-p">$ </span>' + esc(step.cmd.slice(0, i)) + caret;
await sleep(reduceMotion ? 0 : 26);
}
line.innerHTML = '<span class="t-p">$ </span>' + esc(step.cmd);
await sleep(reduceMotion ? 0 : 300);
} else {
line.innerHTML = step.out;
await sleep(reduceMotion ? 0 : 170);
}
}
}
})();
// 3D tilt on the terminal, following the pointer.
const tiltEl = $('termTilt');
if (tiltEl && !reduceMotion && matchMedia('(pointer: fine)').matches) {
tiltEl.addEventListener('pointermove', e => {
const r = tiltEl.getBoundingClientRect();
const px = (e.clientX - r.left) / r.width - 0.5;
const py = (e.clientY - r.top) / r.height - 0.5;
tiltEl.style.transform = `rotateX(${(-py * 5).toFixed(2)}deg) rotateY(${(px * 7).toFixed(2)}deg)`;
});
tiltEl.addEventListener('pointerleave', () => { tiltEl.style.transform = ''; });
}
// Scroll-in reveals, staggered for siblings.
const io = new IntersectionObserver(entries => {
entries.forEach((en, i) => {
if (en.isIntersecting) {
en.target.style.transitionDelay = `${(i % 4) * 70}ms`;
en.target.classList.add('in');
io.unobserve(en.target);
}
});
}, { threshold: 0.12 });
document.querySelectorAll('.reveal').forEach(el => io.observe(el));
$('go').addEventListener('click', runInput);
$('sig').addEventListener('keydown', e => { if (e.key === 'Enter') runInput(); });
// Hero search proxies to the topbar input, so there's exactly one analyze path.
$('heroGo').addEventListener('click', () => { $('sig').value = $('heroSig').value.trim(); runInput(); });
$('heroSig').addEventListener('keydown', e => { if (e.key === 'Enter') $('heroGo').click(); });
let exBusy = false;
document.querySelectorAll('.ex-chip[data-ex]').forEach(c => c.addEventListener('click', async () => {
if (exBusy) return;
exBusy = true;
$('cluster').value = 'mainnet';
$('customRpc').classList.add('hidden');
// Immediate feedback: the engine can cold-start ~10s, so never leave the click
// looking dead. Show the landing spinner right away.
const chips = document.querySelectorAll('.ex-chip');
chips.forEach(x => x.disabled = true);
$('hero').classList.add('hidden'); $('out').classList.add('hidden'); $('txlist').classList.add('hidden');
const st = $('status'); st.className = 'status'; st.classList.remove('hidden');
st.innerHTML = '<span class="spinner"></span>finding a recent transaction to dissect…';
try {
// Pick the program's freshest transaction that actually SUCCEEDED on-chain —
// a failed or empty pick makes a confusing demo.
const res = await api('/signatures/' + c.dataset.ex + clusterQ());
const sigs = res.ok ? await res.json() : [];
const pick = sigs.find(s => !s.err) || sigs[0];
$('sig').value = pick?.signature || c.dataset.ex;
} catch { $('sig').value = c.dataset.ex; }
chips.forEach(x => x.disabled = false);
exBusy = false;
runInput();
}));
// "/" focuses whichever search is on screen.
document.addEventListener('keydown', e => {
if (e.key !== '/' || e.metaKey || e.ctrlKey) return;
const el = document.activeElement;
if (el && /INPUT|SELECT|TEXTAREA/.test(el.tagName)) return;
e.preventDefault();
(!$('hero').classList.contains('hidden') ? $('heroSig') : $('sig')).focus();
});
// Reveal the custom-RPC input only when "Custom RPC…" is selected.
$('cluster').addEventListener('change', () => {
const custom = $('cluster').value === 'custom';
$('customRpc').classList.toggle('hidden', !custom);
if (custom) $('customRpc').focus();
});
// Re-run the current view when the custom endpoint changes (Enter or blur).
$('customRpc').addEventListener('keydown', e => { if (e.key === 'Enter' && currentSig) runInput(); });
$('acctFilter').addEventListener('input', e => filterAccounts(e.target.value));
$('expandAll').addEventListener('click', toggleExpandAll);
$('stageBtn').addEventListener('click', stageAll);
$('simBtn').addEventListener('click', simulate);
$('runSuite').addEventListener('click', runSuite);
$('freezeFixture').addEventListener('click', freezeFixture);
$('exportSuite').addEventListener('click', exportSuite);
$('addScenario').addEventListener('click', addScenarioFromEditor);
</script>
</body>
</html>