Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

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 viem

Replace 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 depth to visualize nested calls.
  • Display step.memory from 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.