@terrariumlabs/core 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/devbar.ts +133 -60
- package/src/engine.js +2 -2
- package/src/worker-runtime.ts +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@terrariumlabs/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "A complete EVM chain inside the page, presented to your dapp as a wallet. revm in WebAssembly, real receipts, byte-identical to Anvil.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"@ethereumjs/util": "^10.1.3",
|
|
62
62
|
"@ethereumjs/mpt": "^10.1.3",
|
|
63
63
|
"@ethereumjs/rlp": "^10.0.0",
|
|
64
|
-
"@terrariumlabs/evm": "^0.
|
|
64
|
+
"@terrariumlabs/evm": "^0.6.0"
|
|
65
65
|
},
|
|
66
66
|
"peerDependencies": {
|
|
67
67
|
"viem": "^2.0.0",
|
package/src/devbar.ts
CHANGED
|
@@ -1,38 +1,55 @@
|
|
|
1
1
|
// devbar.ts — the dev overlay. Plain DOM, own styles, talks to the chain only through provider.request(), so it works
|
|
2
|
-
// on top of any dapp (React or not) and never touches the dapp's code.
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
2
|
+
// on top of any dapp (React or not) and never touches the dapp's code. Parts: the bar (grouped controls: scenario, chain,
|
|
3
|
+
// wallet, the scenario's own buttons, explorer / reset / hide), the transaction explorer (a panel above it listing every
|
|
4
|
+
// transaction with its receipt, decoded call, events and revert reason, fed by `terrarium_transactions`), the scenario
|
|
5
|
+
// selector (`terrarium_scenarios` / `terrarium_selectScenario`) when the Worker runs a list, toasts for every action, and
|
|
6
|
+
// two keyboard shortcuts (Alt+Shift+T: the bar, Alt+Shift+X: the explorer).
|
|
6
7
|
type Provider = { request(a: { method: string; params?: unknown[] }): Promise<any> };
|
|
7
8
|
|
|
8
9
|
const HIDDEN_KEY = 'terrarium:devbar-hidden';
|
|
9
10
|
const CSS = `
|
|
10
11
|
#terrarium-devbar { position: fixed; left: 0; right: 0; bottom: 0; z-index: 2147483000; display: flex; align-items: center; gap: 6px 14px; flex-wrap: wrap; padding: 8px 16px; background: #14231b; color: #dfe9e3; font: 12.5px/1.4 ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif; font-variant-numeric: tabular-nums; box-shadow: 0 -1px 0 rgba(255,255,255,0.08); }
|
|
11
|
-
#terrarium-devbar[hidden], #terrarium-explorer[hidden], #terrarium-devbar-show[hidden] { display: none; }
|
|
12
|
+
#terrarium-devbar[hidden], #terrarium-explorer[hidden], #terrarium-devbar-show[hidden], #terrarium-devbar-menu[hidden] { display: none; }
|
|
13
|
+
#terrarium-devbar *:focus-visible, #terrarium-explorer *:focus-visible, #terrarium-devbar-show:focus-visible { outline: 2px solid #e8c547; outline-offset: 1px; }
|
|
12
14
|
#terrarium-devbar .brand { display: flex; align-items: center; gap: 8px; }
|
|
13
15
|
#terrarium-devbar .tag { background: #e8c547; color: #14231b; font-weight: 700; padding: 2px 8px; border-radius: 6px; letter-spacing: 0.01em; }
|
|
14
16
|
#terrarium-devbar .name { color: #fff; font-weight: 600; }
|
|
15
17
|
#terrarium-devbar select { background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.14); color: #fff; padding: 5px 8px; border-radius: 8px; font: inherit; font-weight: 600; cursor: pointer; max-width: 260px; }
|
|
16
|
-
#terrarium-devbar .status { color: rgba(223,233,227,0.72); white-space: nowrap; }
|
|
18
|
+
#terrarium-devbar .status { display: flex; align-items: center; gap: 10px; color: rgba(223,233,227,0.72); white-space: nowrap; }
|
|
17
19
|
#terrarium-devbar .status b { color: #fff; font-weight: 600; }
|
|
18
20
|
#terrarium-devbar .status .warn { color: #e8c547; }
|
|
21
|
+
#terrarium-devbar .status .shift { color: #e8c547; font-weight: 600; }
|
|
19
22
|
#terrarium-devbar .spacer { flex: 1; min-width: 8px; }
|
|
20
|
-
#terrarium-devbar .group { display: flex; align-items: center; gap: 4px; padding-left: 10px; border-left: 1px solid rgba(255,255,255,0.12); }
|
|
23
|
+
#terrarium-devbar .group { display: flex; align-items: center; gap: 4px; padding-left: 10px; border-left: 1px solid rgba(255,255,255,0.12); position: relative; }
|
|
21
24
|
#terrarium-devbar .group[hidden] { display: none; }
|
|
22
25
|
#terrarium-devbar .glabel { font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em; color: rgba(223,233,227,0.45); margin-right: 4px; user-select: none; }
|
|
23
26
|
#terrarium-devbar .glabel:empty { display: none; }
|
|
24
|
-
#terrarium-devbar
|
|
27
|
+
@media (max-width: 1280px) { #terrarium-devbar .glabel { display: none; } #terrarium-devbar { gap: 6px 10px; padding: 6px 12px; } }
|
|
28
|
+
#terrarium-devbar button, #terrarium-devbar-menu button { background: rgba(255,255,255,0.07); border: 1px solid rgba(255,255,255,0.12); color: #fff; padding: 5px 9px; border-radius: 8px; font: inherit; cursor: pointer; white-space: nowrap; }
|
|
25
29
|
#terrarium-devbar button:hover { background: rgba(255,255,255,0.15); }
|
|
26
30
|
#terrarium-devbar button.on { background: #1f6f5c; border-color: #2b8a73; }
|
|
27
31
|
#terrarium-devbar button.armed { background: #7a3b2a; border-color: #b3452c; }
|
|
28
32
|
#terrarium-devbar button.danger { border-color: rgba(255,140,110,0.4); color: #ffb5a0; }
|
|
33
|
+
#terrarium-devbar button.danger.armed { background: #b3452c; border-color: #ff8c6e; color: #fff; }
|
|
29
34
|
#terrarium-devbar button.quiet { background: transparent; border-color: transparent; color: rgba(223,233,227,0.6); }
|
|
30
35
|
#terrarium-devbar button.quiet:hover { color: #fff; background: rgba(255,255,255,0.08); }
|
|
36
|
+
#terrarium-devbar button .badge { display: inline-block; margin-left: 6px; padding: 0 6px; border-radius: 999px; background: rgba(255,255,255,0.14); font-size: 11px; line-height: 16px; }
|
|
37
|
+
#terrarium-devbar button .badge.bad { background: #b3452c; }
|
|
38
|
+
#terrarium-devbar-menu { position: fixed; z-index: 2147483001; display: flex; flex-direction: column; gap: 2px; padding: 4px; background: #1b2e24; color: #dfe9e3; border: 1px solid rgba(255,255,255,0.14); border-radius: 10px; box-shadow: 0 6px 24px rgba(0,0,0,0.4); font: 12.5px/1.4 ui-sans-serif, system-ui, sans-serif; }
|
|
39
|
+
#terrarium-devbar-menu button { text-align: left; background: transparent; border-color: transparent; }
|
|
40
|
+
#terrarium-devbar-menu button:hover { background: rgba(255,255,255,0.1); }
|
|
31
41
|
#terrarium-devbar-show { position: fixed; right: 12px; bottom: 12px; z-index: 2147483000; width: 36px; height: 36px; border-radius: 50%; border: 1px solid rgba(255,255,255,0.2); background: #14231b; color: #e8c547; font: 16px/1 ui-sans-serif, system-ui, sans-serif; cursor: pointer; box-shadow: 0 2px 8px rgba(0,0,0,0.35); }
|
|
42
|
+
#terrarium-toast { position: fixed; right: 16px; z-index: 2147483001; display: flex; flex-direction: column; gap: 6px; align-items: flex-end; pointer-events: none; font: 12.5px/1.4 ui-sans-serif, system-ui, sans-serif; }
|
|
43
|
+
#terrarium-toast div { background: #1b2e24; color: #dfe9e3; border: 1px solid rgba(255,255,255,0.14); border-left: 3px solid #e8c547; border-radius: 8px; padding: 6px 10px; box-shadow: 0 4px 16px rgba(0,0,0,0.35); opacity: 1; transition: opacity 0.3s; max-width: 420px; }
|
|
44
|
+
#terrarium-toast div.bad { border-left-color: #ff8c6e; }
|
|
45
|
+
#terrarium-toast div.fade { opacity: 0; }
|
|
32
46
|
#terrarium-explorer { position: fixed; left: 0; right: 0; z-index: 2147483000; max-height: 60vh; overflow: auto; background: #0f1a14; color: #dfe9e3; border-top: 1px solid rgba(255,255,255,0.12); font: 12px/1.45 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-variant-numeric: tabular-nums; }
|
|
33
|
-
#terrarium-explorer
|
|
47
|
+
#terrarium-explorer.tall { max-height: 90vh; }
|
|
48
|
+
#terrarium-explorer .head { display: flex; align-items: center; gap: 10px; padding: 8px 16px; font: 13px ui-sans-serif, system-ui, sans-serif; color: rgba(223,233,227,0.8); position: sticky; top: 0; background: #0f1a14; border-bottom: 1px solid rgba(255,255,255,0.08); }
|
|
34
49
|
#terrarium-explorer .head b { color: #fff; }
|
|
35
|
-
#terrarium-explorer .head select { background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.14); color: #fff; padding: 3px
|
|
50
|
+
#terrarium-explorer .head select, #terrarium-explorer .head input, #terrarium-explorer .head button { background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.14); color: #fff; padding: 3px 8px; border-radius: 6px; font: inherit; }
|
|
51
|
+
#terrarium-explorer .head input { width: 220px; } #terrarium-explorer .head input::placeholder { color: rgba(223,233,227,0.45); }
|
|
52
|
+
#terrarium-explorer .head button { cursor: pointer; } #terrarium-explorer .head button:hover { background: rgba(255,255,255,0.15); }
|
|
36
53
|
#terrarium-explorer table { width: 100%; border-collapse: collapse; }
|
|
37
54
|
#terrarium-explorer th { text-align: left; font-weight: 600; color: rgba(223,233,227,0.6); padding: 6px 12px; border-bottom: 1px solid rgba(255,255,255,0.08); white-space: nowrap; }
|
|
38
55
|
#terrarium-explorer th:first-child, #terrarium-explorer td:first-child { padding-left: 16px; }
|
|
@@ -42,6 +59,7 @@ const CSS = `
|
|
|
42
59
|
#terrarium-explorer tr.tx.open td { background: rgba(255,255,255,0.06); border-bottom-color: transparent; }
|
|
43
60
|
#terrarium-explorer .ok { color: #7fd3a8; } #terrarium-explorer .bad { color: #ff9c80; } #terrarium-explorer .wait { color: #e8c547; }
|
|
44
61
|
#terrarium-explorer .hash, #terrarium-explorer .addr { color: #9ecbff; }
|
|
62
|
+
#terrarium-explorer .hash { cursor: copy; } #terrarium-explorer .hash:hover { text-decoration: underline dotted; }
|
|
45
63
|
#terrarium-explorer .name { color: #e8c547; }
|
|
46
64
|
#terrarium-explorer .dim { color: rgba(223,233,227,0.55); }
|
|
47
65
|
#terrarium-explorer .detail td { padding: 10px 16px 14px 40px; white-space: normal; background: rgba(0,0,0,0.25); }
|
|
@@ -66,6 +84,8 @@ export const formatEth = (hex: string | null | undefined) => {
|
|
|
66
84
|
const formatGwei = (hex: string | null | undefined) => { if (!hex) return '0'; const wei = BigInt(hex), whole = wei / 10n ** 9n, frac = (wei % 10n ** 9n).toString().padStart(9, '0').slice(0, 3).replace(/0+$/, ''); return frac ? `${whole}.${frac}` : whole.toString(); };
|
|
67
85
|
const fmtArgs = (args: any): string => Array.isArray(args) ? args.map(fmtArgs).join(', ') : args && typeof args === 'object' ? Object.entries(args).map(([k, v]) => `${k}: ${fmtArgs(v)}`).join(', ') : String(args);
|
|
68
86
|
const time = (hex: string | null) => (hex ? new Date(Number(BigInt(hex)) * 1000).toLocaleTimeString() : '');
|
|
87
|
+
/** a duration in seconds as "+1h 30m" / "-2d" */
|
|
88
|
+
const span = (s: number) => { const a = Math.abs(s), sign = s < 0 ? '-' : '+'; if (a < 3600) return `${sign}${Math.round(a / 60)}m`; if (a < 86400) { const h = Math.floor(a / 3600), m = Math.round((a % 3600) / 60); return `${sign}${h}h${m ? ` ${m}m` : ''}`; } const d = Math.floor(a / 86400), h = Math.round((a % 86400) / 3600); return `${sign}${d}d${h ? ` ${h}h` : ''}`; };
|
|
69
89
|
|
|
70
90
|
export interface DevBarOptions {
|
|
71
91
|
/** start collapsed to the leaf (default false). A Hide/Show click is remembered in localStorage and overrides this */
|
|
@@ -76,69 +96,90 @@ export function mountDevBar(provider: Provider, opts: DevBarOptions = {}) {
|
|
|
76
96
|
if (document.getElementById('terrarium-devbar')) return;
|
|
77
97
|
const rpc = (method: string, params: unknown[] = []) => provider.request({ method, params });
|
|
78
98
|
const bar = document.createElement('footer');
|
|
79
|
-
bar.id = 'terrarium-devbar'; bar.dataset.testid = 'devbar';
|
|
99
|
+
bar.id = 'terrarium-devbar'; bar.dataset.testid = 'devbar'; bar.setAttribute('role', 'toolbar'); bar.setAttribute('aria-label', 'Terrarium dev bar');
|
|
80
100
|
const style = document.createElement('style'); style.textContent = CSS;
|
|
81
101
|
bar.append(style);
|
|
82
102
|
const el = (html: string) => { const t = document.createElement('template'); t.innerHTML = html.trim(); return t.content.firstElementChild as HTMLElement; };
|
|
83
|
-
const btn = (label: string, testid: string, title: string, onClick: () => Promise<unknown> | void) => { const b = el(`<button data-testid="${testid}" title="${esc(title)}">${label}</button>`); b.onclick = () => Promise.resolve(onClick()).catch((e) => console.warn('[terrarium]', e)); return b; };
|
|
84
|
-
const group = (label: string, testid: string, ...children: HTMLElement[]) => { const g = el(`<div class="group" data-testid="${testid}"><span class="glabel">${label}</span></div>`); g.append(...children); return g; };
|
|
103
|
+
const btn = (label: string, testid: string, title: string, onClick: () => Promise<unknown> | void) => { const b = el(`<button data-testid="${testid}" title="${esc(title)}" aria-label="${esc(title)}">${label}</button>`); b.onclick = () => Promise.resolve(onClick()).catch((e) => { console.warn('[terrarium]', e); toast(e?.message ?? String(e), true); }); return b; };
|
|
104
|
+
const group = (label: string, testid: string, ...children: HTMLElement[]) => { const g = el(`<div class="group" data-testid="${testid}" role="group" aria-label="${esc(label || 'tools')}"><span class="glabel">${label}</span></div>`); g.append(...children); return g; };
|
|
105
|
+
|
|
106
|
+
// ---- toasts: every action says what it did --------------------------------------------------------------------------------
|
|
107
|
+
const toasts = el('<div id="terrarium-toast" data-testid="toast" aria-live="polite"></div>');
|
|
108
|
+
const toast = (msg: string, bad = false) => {
|
|
109
|
+
const t = el(`<div${bad ? ' class="bad"' : ''}>${esc(msg)}</div>`); toasts.append(t);
|
|
110
|
+
setTimeout(() => { t.classList.add('fade'); setTimeout(() => t.remove(), 350); }, bad ? 5000 : 2200);
|
|
111
|
+
};
|
|
85
112
|
|
|
86
113
|
// ---- brand + scenario selector + status ---------------------------------------------------------------------------
|
|
87
114
|
const brand = el(`<span class="brand"><span class="tag">Terrarium</span><span class="name" data-testid="scenario-name" hidden></span></span>`);
|
|
88
|
-
const scenarioSelect = el(`<select data-testid="scenario" title="Which scenario the chain runs: switching stores the choice and reloads the page; each scenario keeps its own chain" hidden></select>`) as HTMLSelectElement;
|
|
115
|
+
const scenarioSelect = el(`<select data-testid="scenario" aria-label="Scenario" title="Which scenario the chain runs: switching stores the choice and reloads the page; each scenario keeps its own chain" hidden></select>`) as HTMLSelectElement;
|
|
89
116
|
brand.append(scenarioSelect);
|
|
90
117
|
let scenariosKey = '';
|
|
91
|
-
scenarioSelect.onchange = () => rpc('terrarium_selectScenario', [scenarioSelect.value]).catch((e) => console.warn('[terrarium]', e));
|
|
92
|
-
const status = el(`<span class="status">block <b data-testid="block" data-f="block">…</b>
|
|
118
|
+
scenarioSelect.onchange = () => { toast(`Switching to “${scenarioSelect.value}”…`); rpc('terrarium_selectScenario', [scenarioSelect.value]).catch((e) => { console.warn('[terrarium]', e); toast(e?.message ?? String(e), true); }); };
|
|
119
|
+
const status = el(`<span class="status"><span>block <b data-testid="block" data-f="block">…</b></span><span>chain <span data-f="chain">…</span></span><span data-testid="clock" data-f="clock" title="The chain's clock: the timestamp the next block gets. Yellow when it has been shifted away from wall time (deadlines and oracles see this clock, not yours)"></span><span data-f="engine"></span></span>`);
|
|
93
120
|
|
|
94
121
|
// ---- chain ------------------------------------------------------------------------------------------------------------
|
|
95
|
-
let mining: 'auto' | 'interval' = 'auto', snap: string | null = null;
|
|
122
|
+
let mining: 'auto' | 'interval' = 'auto', snap: { id: string; block: number } | null = null, head = 0;
|
|
96
123
|
const bMining = btn('Blocks: instant', 'mining', 'Auto: a block per transaction. Interval: a block every 3s, so you can watch pending states', async () => {
|
|
97
124
|
mining = mining === 'auto' ? 'interval' : 'auto';
|
|
98
125
|
await (mining === 'auto' ? rpc('evm_setAutomine', [true]) : rpc('evm_setIntervalMining', [3000]));
|
|
99
126
|
bMining.textContent = mining === 'auto' ? 'Blocks: instant' : 'Blocks: every 3s'; bMining.classList.toggle('on', mining === 'interval');
|
|
127
|
+
toast(mining === 'auto' ? 'A block per transaction again' : 'A block every 3 seconds: transactions stay pending in between');
|
|
100
128
|
});
|
|
101
129
|
const bSnap = btn('Snapshot', 'snapshot', 'Snapshot the chain; revert brings blocks, receipts, journal and the UI history back', async () => {
|
|
102
|
-
if (snap) { await rpc('evm_revert', [snap]); snap = null; bSnap.textContent = 'Snapshot'; bSnap.classList.remove('on'); }
|
|
103
|
-
else {
|
|
130
|
+
if (snap) { await rpc('evm_revert', [snap.id]); toast(`Reverted to block ${snap.block}`); snap = null; bSnap.textContent = 'Snapshot'; bSnap.classList.remove('on'); }
|
|
131
|
+
else { const id = await rpc('evm_snapshot'); snap = { id, block: head }; bSnap.textContent = `Revert to block ${head}`; bSnap.classList.add('on'); toast(`Snapshot at block ${head}: the next click reverts to it`); }
|
|
132
|
+
});
|
|
133
|
+
const timeMenu = el('<div id="terrarium-devbar-menu" data-testid="time-menu" role="menu" aria-label="Move the chain clock" hidden></div>'); // on the body: a menu inside the bar was clipped by its layout
|
|
134
|
+
const shift = (seconds: number, label: string, testid: string) => btn(label, testid, `Move the chain clock forward ${label.slice(1)} and mine a block`, async () => { timeMenu.hidden = true; await rpc('evm_increaseTime', [seconds]); await rpc('evm_mine'); toast(`Chain clock moved ${label}; a block sealed at the new time`); });
|
|
135
|
+
timeMenu.append(shift(60, '+1 minute', 'plus-minute'), shift(3600, '+1 hour', 'plus-hour'), shift(86400, '+1 day', 'plus-day'), shift(7 * 86400, '+1 week', 'plus-week'));
|
|
136
|
+
const bTime = btn('Time ▾', 'time', 'Move the chain clock forward: deadlines expire, oracles go stale, interest accrues', () => {
|
|
137
|
+
timeMenu.hidden = !timeMenu.hidden;
|
|
138
|
+
if (!timeMenu.hidden) { const r = bTime.getBoundingClientRect(); timeMenu.style.left = `${r.left}px`; timeMenu.style.bottom = `${window.innerHeight - r.top + 6}px`; }
|
|
104
139
|
});
|
|
140
|
+
bTime.setAttribute('aria-haspopup', 'menu');
|
|
105
141
|
const gChain = group('chain', 'group-chain',
|
|
106
|
-
btn('Mine a block', 'mine', 'Mine one empty block', () => rpc('evm_mine')),
|
|
107
|
-
|
|
108
|
-
|
|
142
|
+
btn('Mine a block', 'mine', 'Mine one empty block', async () => { await rpc('evm_mine'); toast(`Mined block ${head + 1}`); }),
|
|
143
|
+
bTime, bMining, bSnap);
|
|
144
|
+
document.addEventListener('click', (e) => { if (!timeMenu.hidden && !bTime.contains(e.target as Node) && !timeMenu.contains(e.target as Node)) timeMenu.hidden = true; });
|
|
109
145
|
|
|
110
146
|
// ---- wallet -----------------------------------------------------------------------------------------------------------
|
|
111
|
-
const bReject = btn('Reject next tx', 'reject-next', 'The wallet rejects the next signature request (EIP-1193 error 4001), like a user hitting Cancel', () => rpc('terrarium_setWallet', [{ rejectNext: 1 }]));
|
|
112
|
-
const bLatency = btn('Wallet: instant', 'wallet-latency', 'Make the wallet take 2 seconds to answer, like a real one', async () => { const w = await rpc('terrarium_getWallet'); await rpc('terrarium_setWallet', [{ latencyMs: w.latencyMs ? 0 : 2000 }]); });
|
|
113
|
-
const bLag = btn('Receipts: instant', 'receipt-lag', 'Receipts appear 3 seconds after the block, like a node that has not caught up', async () => { const w = await rpc('terrarium_getWallet'); await rpc('terrarium_setWallet', [{ receiptLagMs: w.receiptLagMs ? 0 : 3000 }]); });
|
|
147
|
+
const bReject = btn('Reject next tx', 'reject-next', 'The wallet rejects the next signature request (EIP-1193 error 4001), like a user hitting Cancel', async () => { await rpc('terrarium_setWallet', [{ rejectNext: 1 }]); toast('The wallet will reject the next signature request'); });
|
|
148
|
+
const bLatency = btn('Wallet: instant', 'wallet-latency', 'Make the wallet take 2 seconds to answer, like a real one', async () => { const w = await rpc('terrarium_getWallet'); await rpc('terrarium_setWallet', [{ latencyMs: w.latencyMs ? 0 : 2000 }]); toast(w.latencyMs ? 'The wallet answers instantly again' : 'The wallet now takes 2 seconds to answer'); });
|
|
149
|
+
const bLag = btn('Receipts: instant', 'receipt-lag', 'Receipts appear 3 seconds after the block, like a node that has not caught up', async () => { const w = await rpc('terrarium_getWallet'); await rpc('terrarium_setWallet', [{ receiptLagMs: w.receiptLagMs ? 0 : 3000 }]); toast(w.receiptLagMs ? 'Receipts are immediate again' : 'Receipts now arrive 3 seconds after the block'); });
|
|
114
150
|
const gWallet = group('wallet', 'group-wallet', bReject, bLatency, bLag);
|
|
115
151
|
|
|
116
152
|
// ---- the scenario's own knobs: actors + controls ------------------------------------------------------------------------
|
|
117
|
-
const bActors = btn('Actors off', 'actors', 'Scripted actors: other users, keepers, arbitrageurs trading on their own', () => rpc('terrarium_actors'));
|
|
153
|
+
const bActors = btn('Actors off', 'actors', 'Scripted actors: other users, keepers, arbitrageurs trading on their own', async () => { const on = await rpc('terrarium_actors'); toast(on ? `${actorsLabel} on: other actors are trading now` : `${actorsLabel} off`); });
|
|
154
|
+
let actorsLabel = 'Actors';
|
|
118
155
|
const controls = el('<span class="controls" style="display:contents"></span>'); let controlsKey = '';
|
|
119
156
|
const gScenario = group('scenario', 'group-scenario', bActors, controls);
|
|
120
157
|
|
|
121
158
|
// ---- the transaction explorer -----------------------------------------------------------------------------------------
|
|
122
|
-
const panel = el(`<section id="terrarium-explorer" data-testid="explorer" hidden></section>`);
|
|
159
|
+
const panel = el(`<section id="terrarium-explorer" data-testid="explorer" role="region" aria-label="Transactions" hidden></section>`);
|
|
123
160
|
const open = new Set<string>(); // expanded rows, by hash, kept across refreshes
|
|
124
|
-
let lastRender = '', filter: 'all' | 'mine' | 'failed' = 'all', me: string | null = null; // `me`: accounts[0] from terrarium_status
|
|
125
|
-
const bTxs = btn('Transactions', 'txs', 'Every transaction on this chain, like a block explorer: receipt, decoded call, events, revert reason',
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
});
|
|
129
|
-
const place = () => { panel.style.bottom = `${bar.offsetHeight}px`; };
|
|
161
|
+
let lastRender = '', filter: 'all' | 'mine' | 'failed' = 'all', search = '', me: string | null = null; // `me`: accounts[0] from terrarium_status
|
|
162
|
+
const bTxs = btn('Transactions', 'txs', 'Every transaction on this chain, like a block explorer: receipt, decoded call, events, revert reason (Alt+Shift+X)', () => togglePanel());
|
|
163
|
+
const togglePanel = async (show: boolean = !!panel.hidden) => { panel.hidden = !show; bTxs.classList.toggle('on', show); lastRender = ''; if (show) { await refreshTxs(); panel.querySelector<HTMLInputElement>('[data-testid=tx-search]')?.focus(); } place(); };
|
|
164
|
+
const place = () => { panel.style.bottom = `${bar.offsetHeight}px`; toasts.style.bottom = `${bar.offsetHeight + (panel.hidden ? 0 : panel.offsetHeight) + 12}px`; }; // toasts sit above whatever is open
|
|
130
165
|
const statusCell = (s: string) => s === 'success' ? '<span class="ok" title="success">✓</span>' : s === 'pending' ? '<span class="wait" title="pending">⏳</span>' : `<span class="bad" title="${s}">✗</span>`;
|
|
131
166
|
const who = (labels: Record<string, string>, a: string | null) => a ? `<span class="addr" title="${a}">${esc(labels[a.toLowerCase()] ?? short(a))}</span>` : '<span class="dim">contract creation</span>';
|
|
167
|
+
const matches = (t: any, L: Record<string, string>) => {
|
|
168
|
+
if (!search) return true;
|
|
169
|
+
const q = search.toLowerCase();
|
|
170
|
+
const hay = [t.hash, t.from, t.to, L[t.from?.toLowerCase()], L[t.to?.toLowerCase()], t.method?.name, t.method?.selector, t.revert?.name, t.status, ...(t.logs ?? []).flatMap((l: any) => [l.decoded?.name, L[l.address?.toLowerCase()], l.address])].filter(Boolean).join(' ').toLowerCase();
|
|
171
|
+
return hay.includes(q);
|
|
172
|
+
};
|
|
132
173
|
const render = (data: { total: number; labels: Record<string, string>; transactions: any[] }) => {
|
|
133
|
-
const key = JSON.stringify([data.total, data.transactions.map((t) => [t.hash, t.status]), [...open], filter, me]);
|
|
174
|
+
const key = JSON.stringify([data.total, data.transactions.map((t) => [t.hash, t.status]), [...open], filter, search, me]);
|
|
134
175
|
if (key === lastRender) return; lastRender = key;
|
|
135
176
|
const L = data.labels ?? {};
|
|
136
|
-
const shown = data.transactions.filter((t) => filter === 'all' ? true : filter === 'failed' ? t.status !== 'success' && t.status !== 'pending' : !!me && t.from?.toLowerCase() === me.toLowerCase());
|
|
177
|
+
const shown = data.transactions.filter((t) => (filter === 'all' ? true : filter === 'failed' ? t.status !== 'success' && t.status !== 'pending' : !!me && t.from?.toLowerCase() === me.toLowerCase()) && matches(t, L));
|
|
137
178
|
const rows = shown.map((t) => {
|
|
138
179
|
const method = t.method ? (t.method.name ? `<span class="name">${esc(t.method.name)}</span>` : `<span class="dim">${esc(t.method.selector)}</span>`) : '<span class="dim">transfer</span>';
|
|
139
180
|
const isOpen = open.has(t.hash);
|
|
140
181
|
const detail = !isOpen ? '' : `<tr class="detail" data-testid="tx-detail"><td colspan="10"><dl>
|
|
141
|
-
<dt>hash</dt><dd class="hash">${esc(t.hash)}</dd>
|
|
182
|
+
<dt>hash</dt><dd class="hash" data-copy="${t.hash}" title="click to copy">${esc(t.hash)}</dd>
|
|
142
183
|
<dt>status</dt><dd>${esc(t.status)}${t.status === 'reverted' || t.status === 'dropped' ? ` <span class="bad">· ${t.revert ? esc(t.revert.name) + '(' + esc(fmtArgs(t.revert.args)) + ')' : esc(t.error ?? '')}</span>` : ''}</dd>
|
|
143
184
|
<dt>block</dt><dd>${t.receipt ? `${num(t.receipt.blockNumber)} · ${esc(time(t.timestamp))} · index ${num(t.receipt.transactionIndex)}` : 'pending'}</dd>
|
|
144
185
|
<dt>from</dt><dd>${who(L, t.from)} <span class="dim">${esc(t.from)}</span></dd>
|
|
@@ -153,42 +194,65 @@ export function mountDevBar(provider: Provider, opts: DevBarOptions = {}) {
|
|
|
153
194
|
</td></tr>`;
|
|
154
195
|
return `<tr class="tx${isOpen ? ' open' : ''}" data-testid="tx-row" data-hash="${t.hash}" data-status="${t.status}">
|
|
155
196
|
<td>${statusCell(t.status)}</td><td>${t.receipt ? num(t.receipt.blockNumber) : '<span class="dim">pending</span>'}</td><td class="dim">${esc(time(t.timestamp))}</td>
|
|
156
|
-
<td class="hash" title="${t.hash}">${short(t.hash)}</td><td>${method}</td><td>${who(L, t.from)}</td><td>${who(L, t.to)}</td>
|
|
197
|
+
<td class="hash" data-copy="${t.hash}" title="${t.hash} (click to copy)">${short(t.hash)}</td><td>${method}</td><td>${who(L, t.from)}</td><td>${who(L, t.to)}</td>
|
|
157
198
|
<td>${formatEth(t.value)} ETH</td><td class="dim">${t.receipt ? num(t.receipt.gasUsed) : ''}</td><td class="dim">${t.logs?.length ? `${t.logs.length} event${t.logs.length === 1 ? '' : 's'}` : ''}</td></tr>${detail}`;
|
|
158
199
|
}).join('');
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
<
|
|
162
|
-
|
|
200
|
+
const headEl = panel.querySelector('.head');
|
|
201
|
+
const headHtml = `<b>Transactions</b> <span data-testid="explorer-count">${data.total} on this chain${data.total > data.transactions.length ? ` (showing ${data.transactions.length})` : ''}${filter !== 'all' || search ? `, ${shown.length} shown` : ''}</span>
|
|
202
|
+
<input data-testid="tx-search" type="search" placeholder="filter: hash, address, name, method, event…" aria-label="Filter transactions" value="${esc(search)}">
|
|
203
|
+
<select data-testid="tx-filter" aria-label="Which transactions" title="Which transactions to list"><option value="all"${filter === 'all' ? ' selected' : ''}>all</option><option value="mine"${filter === 'mine' ? ' selected' : ''}>mine (account #0)</option><option value="failed"${filter === 'failed' ? ' selected' : ''}>failed</option></select>
|
|
204
|
+
<span class="spacer" style="flex:1"></span><span class="dim">click a row for the receipt, the decoded call and the events · Esc closes</span>
|
|
205
|
+
<button data-testid="explorer-size" title="Taller / shorter panel">${panel.classList.contains('tall') ? '⇩ shorter' : '⇧ taller'}</button>`;
|
|
206
|
+
const body = rows ? `<table><thead><tr><th></th><th>block</th><th>time</th><th>hash</th><th>method</th><th>from</th><th>to</th><th>value</th><th>gas used</th><th>events</th></tr></thead><tbody>${rows}</tbody></table>` : `<div class="empty">${data.total ? 'Nothing matches.' : 'No transactions yet. Do something in the dapp and it appears here.'}</div>`;
|
|
207
|
+
if (headEl && document.activeElement === headEl.querySelector('[data-testid=tx-search]')) {
|
|
208
|
+
// keep the focused search box: update its neighbours only
|
|
209
|
+
headEl.querySelector('[data-testid=explorer-count]')!.textContent = `${data.total} on this chain${filter !== 'all' || search ? `, ${shown.length} shown` : ''}`;
|
|
210
|
+
panel.querySelector('table, .empty')?.remove(); panel.insertAdjacentHTML('beforeend', body);
|
|
211
|
+
} else panel.innerHTML = `<div class="head">${headHtml}</div>${body}`;
|
|
163
212
|
place();
|
|
164
213
|
};
|
|
165
214
|
panel.addEventListener('change', (e) => { const sel = e.target as HTMLSelectElement; if (sel.dataset.testid === 'tx-filter') { filter = sel.value as typeof filter; lastRender = ''; refreshTxs(); } });
|
|
215
|
+
panel.addEventListener('input', (e) => { const inp = e.target as HTMLInputElement; if (inp.dataset.testid === 'tx-search') { search = inp.value.trim(); lastRender = ''; refreshTxs(); } });
|
|
166
216
|
panel.addEventListener('click', (e) => {
|
|
167
|
-
const
|
|
217
|
+
const target = e.target as HTMLElement;
|
|
218
|
+
const copy = target.closest<HTMLElement>('[data-copy]');
|
|
219
|
+
if (copy) { e.stopPropagation(); navigator.clipboard?.writeText(copy.dataset.copy!).then(() => toast(`Copied ${short(copy.dataset.copy!)}`), () => toast('Clipboard unavailable', true)); return; }
|
|
220
|
+
if (target.closest('[data-testid=explorer-size]')) { panel.classList.toggle('tall'); lastRender = ''; refreshTxs(); return; }
|
|
221
|
+
const row = target.closest<HTMLElement>('tr.tx'); if (!row) return;
|
|
168
222
|
const h = row.dataset.hash!; open.has(h) ? open.delete(h) : open.add(h); lastRender = ''; refreshTxs();
|
|
169
223
|
});
|
|
170
224
|
const refreshTxs = async () => { const data = await rpc('terrarium_transactions', [{ limit: 200 }]).catch(() => null); if (data) render(data); };
|
|
171
225
|
|
|
172
|
-
// ---- reset, hide / show
|
|
173
|
-
|
|
226
|
+
// ---- reset (two clicks), hide / show ------------------------------------------------------------------------------------
|
|
227
|
+
let resetArmed: ReturnType<typeof setTimeout> | null = null;
|
|
228
|
+
const bReset = btn('Reset', 'reset', 'Wipe this scenario\'s chain and boot it again from scratch (click twice)', async () => {
|
|
229
|
+
if (!resetArmed) { bReset.textContent = 'Really reset?'; bReset.classList.add('armed'); toast('Click Reset again within 3 seconds to wipe this scenario\'s chain'); resetArmed = setTimeout(() => { resetArmed = null; bReset.textContent = 'Reset'; bReset.classList.remove('armed'); }, 3000); return; }
|
|
230
|
+
clearTimeout(resetArmed); resetArmed = null; bReset.textContent = 'Resetting…';
|
|
231
|
+
await rpc('terrarium_reset'); location.reload();
|
|
232
|
+
});
|
|
174
233
|
bReset.classList.add('danger');
|
|
175
|
-
const pill = el(`<button id="terrarium-devbar-show" data-testid="show" title="Show the Terrarium dev bar" hidden>🌱</button>`);
|
|
234
|
+
const pill = el(`<button id="terrarium-devbar-show" data-testid="show" title="Show the Terrarium dev bar (Alt+Shift+T)" aria-label="Show the Terrarium dev bar" hidden>🌱</button>`);
|
|
176
235
|
const remember = (hidden: boolean) => { try { localStorage.setItem(HIDDEN_KEY, hidden ? '1' : '0'); } catch {} };
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
document.body.style.paddingBottom = hidden ? '' : '56px';
|
|
181
|
-
remember(hidden);
|
|
182
|
-
};
|
|
183
|
-
const bHide = btn('Hide', 'hide', 'Hide the dev bar (the chain keeps running); the leaf at the bottom right brings it back', () => setHidden(true));
|
|
236
|
+
const apply = (hidden: boolean) => { bar.hidden = hidden; pill.hidden = !hidden; if (hidden) { panel.hidden = true; bTxs.classList.remove('on'); } document.body.style.paddingBottom = hidden ? '' : '56px'; toasts.style.bottom = hidden ? '60px' : `${(bar.offsetHeight || 48) + 12}px`; };
|
|
237
|
+
const setHidden = (hidden: boolean) => { apply(hidden); remember(hidden); };
|
|
238
|
+
const bHide = btn('Hide', 'hide', 'Hide the dev bar (the chain keeps running); the leaf at the bottom right or Alt+Shift+T brings it back', () => setHidden(true));
|
|
184
239
|
bHide.classList.add('quiet');
|
|
185
240
|
pill.onclick = () => setHidden(false);
|
|
186
241
|
const gTools = group('', 'group-tools', bTxs, bReset, bHide);
|
|
187
242
|
|
|
188
243
|
bar.append(brand, status, el('<span class="spacer"></span>'), gChain, gWallet, gScenario, gTools, panel);
|
|
189
|
-
document.body.append(bar, pill);
|
|
244
|
+
document.body.append(bar, pill, toasts, timeMenu);
|
|
190
245
|
let startHidden = !!opts.hidden; try { const v = localStorage.getItem(HIDDEN_KEY); if (v !== null) startHidden = v === '1'; } catch {}
|
|
191
|
-
|
|
246
|
+
apply(startHidden); // without remembering: the default is not a choice
|
|
247
|
+
|
|
248
|
+
// ---- keyboard: Alt+Shift+T the bar, Alt+Shift+X the explorer, Esc closes the explorer / the time menu -----------------------------
|
|
249
|
+
const onKey = (e: KeyboardEvent) => {
|
|
250
|
+
if (!document.getElementById('terrarium-devbar')) { document.removeEventListener('keydown', onKey); return; }
|
|
251
|
+
if (e.altKey && e.shiftKey && (e.code === 'KeyT')) { e.preventDefault(); setHidden(!bar.hidden); }
|
|
252
|
+
else if (e.altKey && e.shiftKey && (e.code === 'KeyX')) { e.preventDefault(); if (bar.hidden) setHidden(false); togglePanel(); }
|
|
253
|
+
else if (e.key === 'Escape') { if (!panel.hidden) togglePanel(false); timeMenu.hidden = true; }
|
|
254
|
+
};
|
|
255
|
+
document.addEventListener('keydown', onKey);
|
|
192
256
|
|
|
193
257
|
// ---- polling: status every 500 ms, the explorer while open, the scenario list now and then -----------------------------------
|
|
194
258
|
const refreshScenarios = async () => {
|
|
@@ -205,32 +269,41 @@ export function mountDevBar(provider: Provider, opts: DevBarOptions = {}) {
|
|
|
205
269
|
};
|
|
206
270
|
const refresh = async () => {
|
|
207
271
|
const s = await rpc('terrarium_status').catch(() => null); if (!s) return;
|
|
208
|
-
me = s.accounts?.[0] ?? me;
|
|
272
|
+
me = s.accounts?.[0] ?? me; head = parseInt(s.block, 16); actorsLabel = s.actorsLabel ?? actorsLabel;
|
|
209
273
|
status.querySelector('[data-f=chain]')!.textContent = String(s.chainId);
|
|
210
|
-
status.querySelector('[data-f=block]')!.textContent = String(
|
|
274
|
+
status.querySelector('[data-f=block]')!.textContent = String(head);
|
|
275
|
+
if (s.now) {
|
|
276
|
+
const chainNow = Number(BigInt(s.now)), drift = chainNow - Math.floor(Date.now() / 1000);
|
|
277
|
+
const clock = status.querySelector<HTMLElement>('[data-f=clock]')!;
|
|
278
|
+
clock.innerHTML = Math.abs(drift) > 90 ? `clock ${esc(new Date(chainNow * 1000).toLocaleString())} <span class="shift">${esc(span(drift))}</span>` : `clock ${esc(new Date(chainNow * 1000).toLocaleTimeString())}`;
|
|
279
|
+
}
|
|
211
280
|
const notes: string[] = [];
|
|
212
281
|
if (s.fork) notes.push(`fork @${s.fork.blockNumber}${s.fork.offline ? ' offline' : ''}${s.fork.misses ? ` · <span class="warn">${s.fork.misses} MISSES</span>` : ''}`);
|
|
213
282
|
if (s.http?.routes) notes.push(`${s.http.routes} HTTP route${s.http.routes === 1 ? '' : 's'}, ${s.http.hits} answered`);
|
|
214
283
|
if (s.restoredFromPersistence) notes.push(`${s.localBlocks} block${s.localBlocks === 1 ? '' : 's'} restored`);
|
|
215
|
-
status.querySelector('[data-f=engine]')!.innerHTML = notes.
|
|
284
|
+
status.querySelector('[data-f=engine]')!.innerHTML = notes.map((n) => `<span>${n}</span>`).join('');
|
|
216
285
|
status.title = `revm/wasm · the chain persists in IndexedDB${s.restoredFromPersistence ? ' (Reset to start clean)' : ''}`;
|
|
286
|
+
if (s.txs) { const bad = s.txs.failed ? `<span class="badge bad" title="${s.txs.failed} failed">${s.txs.failed} ✗</span>` : ''; const pend = s.txs.pending ? `<span class="badge" title="${s.txs.pending} pending">${s.txs.pending} ⏳</span>` : ''; bTxs.innerHTML = `Transactions<span class="badge">${s.txs.total}</span>${pend}${bad}`; }
|
|
217
287
|
const ck = JSON.stringify(s.controls ?? []);
|
|
218
|
-
if (ck !== controlsKey) { controlsKey = ck; controls.replaceChildren(...(s.controls ?? []).map((c: any, i: number) => btn(c.label, `control-${i}`, c.title ?? c.method, () => rpc(c.method, c.params ?? [])))); }
|
|
288
|
+
if (ck !== controlsKey) { controlsKey = ck; controls.replaceChildren(...(s.controls ?? []).map((c: any, i: number) => btn(c.label, `control-${i}`, c.title ?? c.method, async () => { const r = await rpc(c.method, c.params ?? []); toast(`${c.label}${r !== undefined && r !== null && typeof r !== 'object' ? ` → ${r}` : ''}`); }))); }
|
|
219
289
|
bActors.hidden = !s.hasActors; bActors.textContent = `${s.actorsLabel} ${s.actors ? 'on' : 'off'}`; bActors.classList.toggle('on', s.actors);
|
|
220
290
|
gScenario.hidden = !s.hasActors && !(s.controls?.length);
|
|
221
291
|
bReject.textContent = s.wallet.rejectNext > 0 ? `Reject next tx · armed (${s.wallet.rejectNext})` : 'Reject next tx'; bReject.classList.toggle('armed', s.wallet.rejectNext > 0);
|
|
222
292
|
bLatency.textContent = s.wallet.latencyMs ? `Wallet: ${s.wallet.latencyMs / 1000}s delay` : 'Wallet: instant'; bLatency.classList.toggle('on', !!s.wallet.latencyMs);
|
|
223
293
|
bLag.textContent = s.wallet.receiptLagMs ? `Receipts: ${s.wallet.receiptLagMs / 1000}s late` : 'Receipts: instant'; bLag.classList.toggle('on', !!s.wallet.receiptLagMs);
|
|
224
|
-
if (!
|
|
294
|
+
if (!bar.hidden) place();
|
|
295
|
+
if (!panel.hidden) await refreshTxs();
|
|
225
296
|
};
|
|
226
297
|
refresh(); refreshScenarios();
|
|
227
298
|
let ticks = 0;
|
|
228
299
|
const timer = setInterval(() => { if (!document.getElementById('terrarium-devbar')) return clearInterval(timer); refresh(); if (++ticks % 10 === 0) refreshScenarios(); }, 500);
|
|
229
300
|
}
|
|
230
301
|
|
|
231
|
-
/** Remove the dev bar, its explorer and the show pill; restore the page's bottom padding. Idempotent. */
|
|
302
|
+
/** Remove the dev bar, its explorer, the toasts and the show pill; restore the page's bottom padding. Idempotent. */
|
|
232
303
|
export function unmountDevBar() {
|
|
233
304
|
document.getElementById('terrarium-devbar')?.remove();
|
|
234
305
|
document.getElementById('terrarium-devbar-show')?.remove();
|
|
306
|
+
document.getElementById('terrarium-toast')?.remove();
|
|
307
|
+
document.getElementById('terrarium-devbar-menu')?.remove();
|
|
235
308
|
document.body?.style.removeProperty('padding-bottom');
|
|
236
309
|
}
|
package/src/engine.js
CHANGED
|
@@ -458,7 +458,7 @@ export async function createTerrarium(opts = {}) {
|
|
|
458
458
|
return { ...t.rpc, status, receipt: t.receipt, timestamp: b ? hex(b.timestamp) : null, error: t.error ?? null, revertData: t.revertData ?? null };
|
|
459
459
|
});
|
|
460
460
|
const from = before ? all.findIndex((t) => t.hash === before) + 1 : 0;
|
|
461
|
-
return { total: all.length, transactions: all.slice(from, from + Math.max(0, limit)) };
|
|
461
|
+
return { total: all.length, pending: all.filter((t) => t.status === 'pending').length, failed: all.filter((t) => t.status === 'reverted' || t.status === 'dropped').length, transactions: all.slice(from, from + Math.max(0, limit)) };
|
|
462
462
|
}
|
|
463
463
|
|
|
464
464
|
// ---- RPC formatting ----------------------------------------------------------------------------
|
|
@@ -810,7 +810,7 @@ export async function createTerrarium(opts = {}) {
|
|
|
810
810
|
dumpState: () => exclusive(dumpState), loadState: (d) => exclusive(() => loadState(d)), replayJournal: (j) => exclusive(() => replayJournal(j)),
|
|
811
811
|
get journal() { return journal.slice(); }, flush: () => { clearTimeout(persistTimer); return persister ? exclusive(async () => persister.setItem(persistKey, JSON.stringify(await dumpState()))) : Promise.resolve(); },
|
|
812
812
|
get blockNumber() { return latest().number; },
|
|
813
|
-
/** transactions newest first ({ total, transactions }): tx + receipt + status word + revert reason/data + block timestamp */
|
|
813
|
+
/** transactions newest first ({ total, pending, failed, transactions }): tx + receipt + status word + revert reason/data + block timestamp */
|
|
814
814
|
transactions: (o) => listTransactions(o),
|
|
815
815
|
/** React to on-chain events with scripted actors (keepers, oracles, other users, bridges...). */
|
|
816
816
|
onLog(filter, handler) { const l = { filter, handler }; logListeners.push(l); return () => logListeners.splice(logListeners.indexOf(l), 1); },
|
package/src/worker-runtime.ts
CHANGED
|
@@ -108,7 +108,7 @@ export async function runScenario(input: ScenarioInput, opts: { storage?: Storag
|
|
|
108
108
|
|
|
109
109
|
// ---- generic controls, reachable through the provider like any RPC method -----------------------------------
|
|
110
110
|
sim.addMethod('terrarium_actors', async (on?: boolean) => { await actors.toggle(on ?? !actors.enabled); return actors.enabled; });
|
|
111
|
-
sim.addMethod('terrarium_status', async () => ({ scenario: scenarioName, chainId, engine: sim.engine, block: toHex(sim.blockNumber), accounts: ctx.accounts, actors: actors.enabled, actorsLabel: config.actorsLabel ?? 'Actors', hasActors: toggled.length > 0, wallet: { ...sim.wallet }, controls: config.controls ?? [], restoredFromPersistence: sim.restoredFromPersistence, localBlocks: Number(sim.blockNumber) - (config.fork ? config.fork.blockNumber + 1 : 0), http: { routes: httpRoutes.length, hits: httpHits }, fork: config.fork ? { blockNumber: config.fork.blockNumber, offline: !!config.fork.offline, misses: sim.offlineMisses.length } : null, ...(await config.status?.(ctx)) }));
|
|
111
|
+
sim.addMethod('terrarium_status', async () => ({ scenario: scenarioName, chainId, engine: sim.engine, block: toHex(sim.blockNumber), now: toHex(sim.now()), txs: (({ total, pending, failed }) => ({ total, pending, failed }))(sim.transactions({ limit: 0 })), accounts: ctx.accounts, actors: actors.enabled, actorsLabel: config.actorsLabel ?? 'Actors', hasActors: toggled.length > 0, wallet: { ...sim.wallet }, controls: config.controls ?? [], restoredFromPersistence: sim.restoredFromPersistence, localBlocks: Number(sim.blockNumber) - (config.fork ? config.fork.blockNumber + 1 : 0), http: { routes: httpRoutes.length, hits: httpHits }, fork: config.fork ? { blockNumber: config.fork.blockNumber, offline: !!config.fork.offline, misses: sim.offlineMisses.length } : null, ...(await config.status?.(ctx)) }));
|
|
112
112
|
// ---- the transaction explorer: the engine's list, decoded (address's own ABI, then `abis`, then the known set) and labelled
|
|
113
113
|
const configAbi = (config.abis ?? []).flat() as Abi;
|
|
114
114
|
const abisFor = (address: string | null): Abi[] => [registry.get((address ?? '').toLowerCase())?.abi, configAbi, KNOWN_ABI].filter((a): a is Abi => !!a && a.length > 0);
|
|
@@ -127,8 +127,8 @@ export async function runScenario(input: ScenarioInput, opts: { storage?: Storag
|
|
|
127
127
|
// events: every candidate with the log's topic0 is tried (ERC-20 and ERC-721 Transfer share it and differ in indexed params)
|
|
128
128
|
const decodeLog = (l: { address: string; topics: Hex[]; data: Hex }) => first(abisFor(l.address), (abi: Abi) => first(abi.filter((i) => i.type === 'event' && toEventSelector(i as any) === l.topics[0]), (item: any) => { const d: any = decodeEventLog({ abi: [item], data: l.data, topics: l.topics as [Hex, ...Hex[]] }); return { name: d.eventName as string, args: plain(d.args ?? {}) }; }));
|
|
129
129
|
sim.addMethod('terrarium_transactions', (opts?: { limit?: number; before?: string }) => {
|
|
130
|
-
const { total, transactions } = sim.transactions(opts ?? {});
|
|
131
|
-
return { total, labels: labelsOf(), transactions: transactions.map((t: any) => {
|
|
130
|
+
const { total, pending, failed, transactions } = sim.transactions(opts ?? {});
|
|
131
|
+
return { total, pending, failed, labels: labelsOf(), transactions: transactions.map((t: any) => {
|
|
132
132
|
const hasData = t.input && t.input.length >= 10;
|
|
133
133
|
const method = !t.to ? { name: 'create', args: [] } : hasData ? decodeCall(t.to, t.input) ?? { name: null, selector: t.input.slice(0, 10) } : null;
|
|
134
134
|
const revert = t.status === 'reverted' && t.revertData && t.revertData !== '0x' ? decodeRevert(t.to, t.revertData) : null;
|