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

A minimal EVM debugger using Svelte and Tevm Node. Shows live opcode execution, stack, memory, errors, and gas usage.

Project Setup

npm create vite@latest tevm-debugger -- --template svelte-ts
cd tevm-debugger
npm install tevm tevm/contract

Components

EVMDebugger.svelte

<script lang="ts">
  import { onMount, onDestroy } from 'svelte'
  import { createTevmNode } from 'tevm/node'
  import { createImpersonatedTx } from 'tevm/tx'
  import type { InterpreterStep } from 'tevm/evm'
 
  let steps: InterpreterStep[] = []
  let currentStep: InterpreterStep | null = null
  let errors: string[] = []
  let gasUsed = 0n
  let isRunning = false
 
  const node = createTevmNode()
  let vm: Awaited<ReturnType<typeof node.getVm>>
 
  onMount(async () => {
    vm = await node.getVm()
    vm.evm.events?.on('step', (step, next) => {
      currentStep = step
      steps = [...steps, step]
      next?.()
    })
    vm.evm.events?.on('afterMessage', (result, next) => {
      if (result.execResult.exceptionError) {
        errors = [...errors, result.execResult.exceptionError.error]
      }
      gasUsed = result.execResult.executionGasUsed
      next?.()
    })
  })
 
  onDestroy(() => vm?.evm.events?.removeAllListeners())
 
  async function runSampleTx() {
    isRunning = true
    steps = []
    errors = []
    try {
      const tx = createImpersonatedTx({ to: '0x1234...', data: '0x...' })
      await vm.runTx({ tx })
    } catch (error) {
      errors = [...errors, error.message]
    }
    isRunning = false
  }
</script>
 
<div class="debugger">
  <div class="controls">
    <button on:click={runSampleTx} disabled={isRunning}>
      {isRunning ? 'Running...' : 'Run Transaction'}
    </button>
    <div class="gas">Gas Used: {gasUsed.toString()}</div>
  </div>
 
  <div class="execution">
    <h3>Current Step</h3>
    {#if currentStep}
      <div class="step">
        <div>PC: {currentStep.pc}</div>
        <div>Opcode: {currentStep.opcode.name}</div>
        <div>Gas Left: {currentStep.gasLeft.toString()}</div>
        <div>Depth: {currentStep.depth}</div>
      </div>
    {/if}
  </div>
 
  <div class="stack">
    <h3>Stack</h3>
    {#if currentStep?.stack}
      <div class="stack-items">
        {#each currentStep.stack as item}
          <div class="stack-item">{item.toString(16)}</div>
        {/each}
      </div>
    {/if}
  </div>
 
  <div class="errors">
    <h3>Errors</h3>
    {#each errors as error}
      <div class="error">{error}</div>
    {/each}
  </div>
 
  <div class="history">
    <h3>Execution History ({steps.length} steps)</h3>
    <div class="steps">
      {#each steps as step}
        <div class="history-step">
          {step.opcode.name} (Gas: {step.gasLeft.toString()})
        </div>
      {/each}
    </div>
  </div>
</div>
 
<style>
  .debugger { padding: 1rem; display: grid; gap: 1rem; grid-template-columns: repeat(2, 1fr); }
  .controls { grid-column: 1 / -1; display: flex; justify-content: space-between; align-items: center; }
  button { padding: 0.5rem 1rem; background: #4a5568; color: white; border: none; border-radius: 0.25rem; cursor: pointer; }
  button:disabled { opacity: 0.5; cursor: not-allowed; }
  .execution, .stack, .errors, .history { background: #2d3748; padding: 1rem; border-radius: 0.5rem; color: #e2e8f0; }
  .stack-items { display: flex; flex-direction: column; gap: 0.5rem; }
  .stack-item { font-family: monospace; padding: 0.25rem; background: #4a5568; border-radius: 0.25rem; }
  .error { color: #fc8181; padding: 0.5rem; margin: 0.25rem 0; background: #742a2a; border-radius: 0.25rem; }
  .steps { height: 200px; overflow-y: auto; }
  .history-step { padding: 0.25rem; border-bottom: 1px solid #4a5568; font-family: monospace; }
</style>

App.svelte

<script lang="ts">
  import EVMDebugger from './lib/EVMDebugger.svelte'
</script>
 
<main>
  <h1>Tevm Debugger</h1>
  <EVMDebugger />
</main>
 
<style>
  main { max-width: 1200px; margin: 0 auto; padding: 2rem; }
  h1 { color: #2d3748; margin-bottom: 2rem; }
</style>

Advanced Features

Memory Viewer Component

<script lang="ts">
  export let memory: Uint8Array
  export let startOffset = 0
  export let bytesPerRow = 16
 
  $: rows = chunk(memory, bytesPerRow)
 
  function chunk(array: Uint8Array, size: number) {
    const chunks = []
    for (let i = 0; i < array.length; i += size) chunks.push(array.slice(i, i + size))
    return chunks
  }
  const formatByte = (b: number) => b.toString(16).padStart(2, '0')
  const formatAscii = (b: number) => (b >= 32 && b <= 126 ? String.fromCharCode(b) : '.')
</script>
 
<div class="memory-viewer">
  {#each rows as row, i}
    <div class="memory-row">
      <span class="offset">{(startOffset + i * bytesPerRow).toString(16).padStart(8, '0')}:</span>
      <span class="hex">{#each row as byte}{formatByte(byte)}{/each}</span>
      <span class="ascii">{#each row as byte}{formatAscii(byte)}{/each}</span>
    </div>
  {/each}
</div>
 
<style>
  .memory-viewer { font-family: monospace; white-space: pre; }
  .memory-row { display: flex; gap: 1rem; padding: 0.25rem 0; }
  .offset { color: #718096; }
  .hex { letter-spacing: 0.1em; }
  .ascii { color: #718096; }
</style>

Storage Viewer Component

<script lang="ts">
  import type { Address } from 'tevm/utils'
  import { createTevmNode } from 'tevm/node'
 
  export let address: Address
  let storage = new Map<string, string>()
  let loading = false
  const node = createTevmNode()
 
  async function loadStorage() {
    loading = true
    try {
      const vm = await node.getVm()
      const dump = await vm.stateManager.dumpStorage(address)
      storage = new Map(Object.entries(dump))
    } catch (error) {
      console.error('Failed to load storage:', error)
    }
    loading = false
  }
</script>
 
<div class="storage">
  <button on:click={loadStorage} disabled={loading}>
    {loading ? 'Loading...' : 'Load Storage'}
  </button>
  {#if storage.size > 0}
    <div class="storage-items">
      {#each [...storage] as [slot, value]}
        <div class="storage-item">
          <span class="slot">{slot}:</span>
          <span class="value">{value}</span>
        </div>
      {/each}
    </div>
  {/if}
</div>
 
<style>
  .storage { padding: 1rem; }
  .storage-items { margin-top: 1rem; }
  .storage-item { display: flex; gap: 1rem; padding: 0.25rem 0; font-family: monospace; }
  .slot { color: #718096; }
</style>

Usage

Project structure:

tevm-debugger/
├── src/
│   ├── lib/
│   │   ├── EVMDebugger.svelte
│   │   ├── MemoryViewer.svelte
│   │   └── StorageViewer.svelte
│   ├── App.svelte
│   └── main.ts
└── package.json

Run npm run dev, then use the debugger:

import { createImpersonatedTx } from 'tevm/tx'
 
const deployTx = createImpersonatedTx({ data: bytecode })
await vm.runTx({ tx: deployTx })
 
const callTx = createImpersonatedTx({ to: '0x...', data: '0x...' })
await vm.runTx({ tx: callTx })

Customization

Adding Transaction History

<script lang="ts">
  import { writable } from 'svelte/store'
 
  const transactions = writable<{ hash: string; to: string; data: string; status: 'success' | 'error' }[]>([])
 
  vm.evm.events?.on('afterMessage', (result, next) => {
    transactions.update(txs => [...txs, {
      hash: result.execResult.hash?.toString() ?? '',
      to: result.execResult.to?.toString() ?? '',
      data: result.execResult.data?.toString('hex') ?? '',
      status: result.execResult.exceptionError ? 'error' : 'success'
    }])
    next?.()
  })
</script>
 
<div class="transactions">
  <h3>Transaction History</h3>
  {#each $transactions as tx}
    <div class="transaction" class:error={tx.status === 'error'}>
      <div>Hash: {tx.hash}</div>
      <div>To: {tx.to}</div>
      <div>Data: {tx.data}</div>
    </div>
  {/each}
</div>

Adding Gas Profiling

<script lang="ts">
  const gasProfile = new Map<string, { count: number; totalGas: bigint }>()
 
  vm.evm.events?.on('step', (step, next) => {
    const stats = gasProfile.get(step.opcode.name) ?? { count: 0, totalGas: 0n }
    stats.count++
    stats.totalGas += BigInt(step.opcode.fee)
    gasProfile.set(step.opcode.name, stats)
    next?.()
  })
</script>
 
<div class="gas-profile">
  <h3>Gas Profile</h3>
  <table>
    <thead><tr><th>Opcode</th><th>Count</th><th>Total Gas</th></tr></thead>
    <tbody>
      {#each [...gasProfile] as [opcode, stats]}
        <tr><td>{opcode}</td><td>{stats.count}</td><td>{stats.totalGas.toString()}</td></tr>
      {/each}
    </tbody>
  </table>
</div>

Related