Skip to main content
Tevm integrates with Svelte using stores and SvelteKit’s load functions for server-side data fetching.

Installation

bun add @tevm/voltaire

Basic Store Pattern

Create reactive stores for Ethereum data:
// lib/ethereum.ts
import { writable, derived } from 'svelte/store'
import { Address } from '@tevm/voltaire/Address'
import { Wei } from '@tevm/voltaire/Denomination'
import { createJsonRpcProvider } from '@tevm/voltaire/jsonrpc'

const provider = createJsonRpcProvider('https://eth.llamarpc.com')

export function createBalanceStore(address: string) {
  const { subscribe, set } = writable<string | null>(null)
  const loading = writable(true)
  const error = writable<Error | null>(null)

  async function fetch() {
    loading.set(true)
    error.set(null)
    try {
      const addr = Address.from(address)
      const balance = await provider.eth.getBalance({ address: addr })
      set(Wei.toEther(balance))
    } catch (e) {
      error.set(e as Error)
    } finally {
      loading.set(false)
    }
  }

  fetch()

  return {
    subscribe,
    loading,
    error,
    refresh: fetch,
  }
}
<!-- Balance.svelte -->
<script lang="ts">
  import { createBalanceStore } from '$lib/ethereum'

  export let address: string

  $: balance = createBalanceStore(address)
</script>

{#if $balance.loading}
  <p>Loading...</p>
{:else if $balance.error}
  <p>Error: {$balance.error.message}</p>
{:else}
  <p>Balance: {$balance} ETH</p>
  <button on:click={balance.refresh}>Refresh</button>
{/if}

SvelteKit Load Functions

Fetch data server-side with SvelteKit:
// routes/address/[address]/+page.server.ts
import { Address } from '@tevm/voltaire/Address'
import { Wei } from '@tevm/voltaire/Denomination'
import { createJsonRpcProvider } from '@tevm/voltaire/jsonrpc'
import type { PageServerLoad } from './$types'

const provider = createJsonRpcProvider('https://eth.llamarpc.com')

export const load: PageServerLoad = async ({ params }) => {
  const addr = Address.from(params.address)

  const [balance, nonce, code] = await Promise.all([
    provider.eth.getBalance({ address: addr }),
    provider.eth.getTransactionCount({ address: addr }),
    provider.eth.getCode({ address: addr }),
  ])

  return {
    address: params.address,
    balance: Wei.toEther(balance),
    nonce: Number(nonce),
    isContract: code.length > 2,
  }
}
<!-- routes/address/[address]/+page.svelte -->
<script lang="ts">
  import type { PageData } from './$types'
  export let data: PageData
</script>

<h1>Address: {data.address}</h1>
<p>Balance: {data.balance} ETH</p>
<p>Nonce: {data.nonce}</p>
<p>Type: {data.isContract ? 'Contract' : 'EOA'}</p>

Reactive Polling

Poll for block updates:
// lib/blocks.ts
import { readable } from 'svelte/store'
import { createJsonRpcProvider } from '@tevm/voltaire/jsonrpc'

const provider = createJsonRpcProvider('https://eth.llamarpc.com')

export const latestBlock = readable<bigint | null>(null, (set) => {
  const fetchBlock = async () => {
    const blockNumber = await provider.eth.getBlockNumber()
    set(blockNumber)
  }

  fetchBlock()
  const interval = setInterval(fetchBlock, 12000)

  return () => clearInterval(interval)
})
<script lang="ts">
  import { latestBlock } from '$lib/blocks'
</script>

<p>Latest Block: {$latestBlock?.toString() ?? 'Loading...'}</p>

Derived Stores

Compute values from multiple stores:
// lib/account.ts
import { derived, writable } from 'svelte/store'
import { Address } from '@tevm/voltaire/Address'
import { Wei } from '@tevm/voltaire/Denomination'
import { createJsonRpcProvider } from '@tevm/voltaire/jsonrpc'

const provider = createJsonRpcProvider('https://eth.llamarpc.com')

export const currentAddress = writable<string | null>(null)

export const accountData = derived(
  currentAddress,
  ($address, set) => {
    if (!$address) {
      set(null)
      return
    }

    const fetchData = async () => {
      const addr = Address.from($address)
      const [balance, nonce] = await Promise.all([
        provider.eth.getBalance({ address: addr }),
        provider.eth.getTransactionCount({ address: addr }),
      ])

      set({
        address: $address,
        balance: Wei.toEther(balance),
        nonce: Number(nonce),
      })
    }

    fetchData()
  },
  null as { address: string; balance: string; nonce: number } | null
)

Custom Async Store

Reusable async store factory:
// lib/asyncStore.ts
import { writable, type Readable } from 'svelte/store'

interface AsyncStore<T> extends Readable<T | null> {
  loading: Readable<boolean>
  error: Readable<Error | null>
  refresh: () => Promise<void>
}

export function asyncStore<T>(fetcher: () => Promise<T>): AsyncStore<T> {
  const data = writable<T | null>(null)
  const loading = writable(true)
  const error = writable<Error | null>(null)

  async function refresh() {
    loading.set(true)
    error.set(null)
    try {
      const result = await fetcher()
      data.set(result)
    } catch (e) {
      error.set(e as Error)
    } finally {
      loading.set(false)
    }
  }

  refresh()

  return {
    subscribe: data.subscribe,
    loading: { subscribe: loading.subscribe },
    error: { subscribe: error.subscribe },
    refresh,
  }
}
// Usage
import { asyncStore } from '$lib/asyncStore'
import { Address } from '@tevm/voltaire/Address'
import { createJsonRpcProvider } from '@tevm/voltaire/jsonrpc'

const provider = createJsonRpcProvider('https://eth.llamarpc.com')

const balance = asyncStore(() =>
  provider.eth.getBalance({
    address: Address.from('0x...')
  })
)

Actions for Transactions

Use Svelte actions for form submissions:
<script lang="ts">
  import { createJsonRpcProvider } from '@tevm/voltaire/jsonrpc'
  import { Address } from '@tevm/voltaire/Address'

  let to = ''
  let amount = ''
  let status = ''

  async function handleSend() {
    status = 'Sending...'
    try {
      // Your transaction logic here
      status = 'Sent!'
    } catch (e) {
      status = `Error: ${(e as Error).message}`
    }
  }
</script>

<form on:submit|preventDefault={handleSend}>
  <input bind:value={to} placeholder="To address" />
  <input bind:value={amount} placeholder="Amount ETH" />
  <button type="submit">Send</button>
</form>
<p>{status}</p>

Context API

Share provider across components:
// lib/context.ts
import { getContext, setContext } from 'svelte'
import { createJsonRpcProvider } from '@tevm/voltaire/jsonrpc'

const PROVIDER_KEY = Symbol('provider')

export function setProvider(url: string) {
  const provider = createJsonRpcProvider(url)
  setContext(PROVIDER_KEY, provider)
  return provider
}

export function getProvider() {
  return getContext(PROVIDER_KEY)
}
<!-- +layout.svelte -->
<script lang="ts">
  import { setProvider } from '$lib/context'
  setProvider('https://eth.llamarpc.com')
</script>

<slot />

Svelte 5 Runes

With Svelte 5 runes:
<script lang="ts">
  import { Address } from '@tevm/voltaire/Address'
  import { Wei } from '@tevm/voltaire/Denomination'
  import { createJsonRpcProvider } from '@tevm/voltaire/jsonrpc'

  const provider = createJsonRpcProvider('https://eth.llamarpc.com')

  let address = $state('')
  let balance = $state<string | null>(null)
  let loading = $state(false)

  async function fetchBalance() {
    if (!address) return
    loading = true
    try {
      const addr = Address.from(address)
      const result = await provider.eth.getBalance({ address: addr })
      balance = Wei.toEther(result)
    } finally {
      loading = false
    }
  }

  $effect(() => {
    if (address.length === 42) {
      fetchBalance()
    }
  })
</script>

<input bind:value={address} placeholder="Enter address" />

{#if loading}
  <p>Loading...</p>
{:else if balance}
  <p>Balance: {balance} ETH</p>
{/if}

Next Steps