Building a Debugger UI
This Svelte example runs a real EVM call in the browser and renders its opcode, stack, gas, and trace data. It uses the public MemoryClient call API from tevm@1.0.0-rc.151; it does not depend on internal VM event emitters.
Create the App
npm create vite@latest tevm-debugger -- --template svelte-ts
cd tevm-debugger
npm install
npm install tevm@1.0.0-rc.151 viemReplace src/App.svelte with the component below.
Debugger Component
<script lang="ts">
import { createMemoryClient, PREFUNDED_ACCOUNTS } from 'tevm'
type DisplayStep = {
pc: number
opcode: string
gasLeft: bigint
depth: number
stack: bigint[]
}
const client = createMemoryClient()
const contract = '0x4444444444444444444444444444444444444444'
let steps: DisplayStep[] = []
let error = ''
let gasUsed = 0n
let running = false
async function run() {
running = true
steps = []
error = ''
try {
// PUSH1 1; PUSH1 0; SSTORE
await client.tevmSetAccount({
address: contract,
deployedBytecode: '0x6001600055',
})
const result = await client.tevmCall({
from: PREFUNDED_ACCOUNTS[0].address,
to: contract,
createTrace: true,
throwOnFail: false,
onStep(step, next) {
steps = [
...steps,
{
pc: step.pc,
opcode: step.opcode.name,
gasLeft: step.gasLeft,
depth: step.depth,
stack: Array.from(step.stack),
},
]
next?.()
},
})
gasUsed = result.executionGasUsed
error = result.errors?.map((item) => item.message).join('\n') ?? ''
} catch (cause) {
error = cause instanceof Error ? cause.message : String(cause)
} finally {
running = false
}
}
</script>
<svelte:head><title>Tevm Debugger</title></svelte:head>
<main>
<h1>Tevm EVM Debugger</h1>
<button onclick={run} disabled={running}>
{running ? 'Running…' : 'Run sample bytecode'}
</button>
<p>Execution gas used: {gasUsed.toString()}</p>
{#if error}
<pre class="error">{error}</pre>
{/if}
<table>
<thead>
<tr><th>PC</th><th>Opcode</th><th>Gas left</th><th>Depth</th><th>Stack</th></tr>
</thead>
<tbody>
{#each steps as step}
<tr>
<td>{step.pc}</td>
<td>{step.opcode}</td>
<td>{step.gasLeft.toString()}</td>
<td>{step.depth}</td>
<td>{step.stack.map((value) => `0x${value.toString(16)}`).join(', ') || 'none'}</td>
</tr>
{/each}
</tbody>
</table>
</main>
<style>
:global(body) { margin: 0; background: #111827; color: #e5e7eb; font-family: system-ui; }
main { max-width: 72rem; margin: 0 auto; padding: 2rem; }
button { padding: .65rem 1rem; border: 0; border-radius: .4rem; cursor: pointer; }
button:disabled { cursor: wait; opacity: .6; }
table { width: 100%; margin-top: 1.5rem; border-collapse: collapse; font-family: ui-monospace, monospace; }
th, td { padding: .65rem; border-bottom: 1px solid #374151; text-align: left; }
.error { padding: 1rem; background: #7f1d1d; white-space: pre-wrap; }
</style>The call executes three instructions, so the table should show PUSH1, PUSH1, and SSTORE. Array.from(step.stack) copies each live interpreter stack into stable display data.
Use the Returned Trace
The same call can return a serializable Geth-style trace. It contains pc, op, gas, gasCost, depth, and stack values for each instruction.
import { createMemoryClient, PREFUNDED_ACCOUNTS } from 'tevm'
const client = createMemoryClient()
const contract = '0x4444444444444444444444444444444444444444'
await client.tevmSetAccount({
address: contract,
deployedBytecode: '0x6001600055',
})
const result = await client.tevmCall({
from: PREFUNDED_ACCOUNTS[0].address,
to: contract,
createTrace: true,
})
for (const step of result.trace?.structLogs ?? []) {
console.log(step.pc, step.op, step.gas, step.stack)
}Use the callback for a live UI and the returned trace for export, filtering, comparisons, or persistence. Always call next?.() from a callback so execution can continue.
Next Steps
- Add a source map from bytecode offsets to Solidity locations.
- Group trace steps by
depthto visualize nested calls. - Display
step.memoryfrom the live callback when building a memory inspector. - Persist traces as JSON after converting bigint fields to strings.
See EVM Events, Call API, and Performance Profiling for the underlying APIs.

