Skip to main content
Tevm integrates with Vue using the Composition API and Nuxt for server-side rendering.

Installation

bun add @tevm/voltaire

Basic Composable

Create reusable composables for Ethereum data:
// composables/useBalance.ts
import { ref, watch, type Ref } from 'vue'
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 useBalance(address: Ref<string> | string) {
  const balance = ref<string | null>(null)
  const loading = ref(true)
  const error = ref<Error | null>(null)

  async function fetch() {
    const addr = typeof address === 'string' ? address : address.value
    if (!addr) return

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

  if (typeof address !== 'string') {
    watch(address, fetch, { immediate: true })
  } else {
    fetch()
  }

  return { balance, loading, error, refresh: fetch }
}
<!-- BalanceDisplay.vue -->
<script setup lang="ts">
import { useBalance } from '@/composables/useBalance'

const props = defineProps<{ address: string }>()
const { balance, loading, error, refresh } = useBalance(() => props.address)
</script>

<template>
  <div v-if="loading">Loading...</div>
  <div v-else-if="error">Error: {{ error.message }}</div>
  <div v-else>
    <p>Balance: {{ balance }} ETH</p>
    <button @click="refresh">Refresh</button>
  </div>
</template>

Nuxt Server Routes

Fetch data server-side with Nuxt:
// server/api/address/[address].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')

export default defineEventHandler(async (event) => {
  const address = getRouterParam(event, 'address')
  if (!address) throw createError({ statusCode: 400, message: 'Address required' })

  const addr = Address.from(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,
    balance: Wei.toEther(balance),
    nonce: Number(nonce),
    isContract: code.length > 2,
  }
})
<!-- pages/address/[address].vue -->
<script setup lang="ts">
const route = useRoute()
const { data, pending, error } = await useFetch(`/api/address/${route.params.address}`)
</script>

<template>
  <div v-if="pending">Loading...</div>
  <div v-else-if="error">Error loading address</div>
  <div v-else>
    <h1>{{ data.address }}</h1>
    <p>Balance: {{ data.balance }} ETH</p>
    <p>Nonce: {{ data.nonce }}</p>
    <p>Type: {{ data.isContract ? 'Contract' : 'EOA' }}</p>
  </div>
</template>

Polling with useIntervalFn

Poll for block updates using VueUse:
// composables/useLatestBlock.ts
import { ref } from 'vue'
import { useIntervalFn } from '@vueuse/core'
import { createJsonRpcProvider } from '@tevm/voltaire/jsonrpc'

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

export function useLatestBlock() {
  const blockNumber = ref<bigint | null>(null)
  const loading = ref(true)

  async function fetch() {
    try {
      blockNumber.value = await provider.eth.getBlockNumber()
    } finally {
      loading.value = false
    }
  }

  fetch()
  useIntervalFn(fetch, 12000)

  return { blockNumber, loading }
}

Provide/Inject Pattern

Share provider across components:
// plugins/ethereum.ts
import { createJsonRpcProvider } from '@tevm/voltaire/jsonrpc'
import type { InjectionKey } from 'vue'

export const providerKey: InjectionKey<ReturnType<typeof createJsonRpcProvider>> = Symbol('provider')

export default defineNuxtPlugin((nuxtApp) => {
  const provider = createJsonRpcProvider('https://eth.llamarpc.com')
  nuxtApp.vueApp.provide(providerKey, provider)
})
// composables/useProvider.ts
import { inject } from 'vue'
import { providerKey } from '@/plugins/ethereum'

export function useProvider() {
  const provider = inject(providerKey)
  if (!provider) throw new Error('Provider not found')
  return provider
}

Multiple Addresses

Fetch multiple addresses in parallel:
// composables/useMultipleBalances.ts
import { ref, computed, type Ref } from 'vue'
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 useMultipleBalances(addresses: Ref<string[]>) {
  const balances = ref<Map<string, string>>(new Map())
  const loading = ref(true)

  async function fetchAll() {
    loading.value = true
    const results = await Promise.all(
      addresses.value.map(async (addr) => {
        const parsedAddr = Address.from(addr)
        const balance = await provider.eth.getBalance({ address: parsedAddr })
        return [addr, Wei.toEther(balance)] as const
      })
    )
    balances.value = new Map(results)
    loading.value = false
  }

  watch(addresses, fetchAll, { immediate: true, deep: true })

  return { balances, loading, refresh: fetchAll }
}

Async Component Pattern

Load data on component mount:
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { Address } from '@tevm/voltaire/Address'
import { createJsonRpcProvider } from '@tevm/voltaire/jsonrpc'

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

const props = defineProps<{ address: string }>()

const code = ref<string | null>(null)
const isContract = ref(false)

onMounted(async () => {
  const addr = Address.from(props.address)
  code.value = await provider.eth.getCode({ address: addr })
  isContract.value = code.value.length > 2
})
</script>

<template>
  <div>
    <span v-if="isContract">📄 Contract</span>
    <span v-else>👤 EOA</span>
  </div>
</template>

Pinia Store

Use Pinia for global state:
// stores/ethereum.ts
import { defineStore } from 'pinia'
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 useEthereumStore = defineStore('ethereum', {
  state: () => ({
    balances: {} as Record<string, string>,
    blockNumber: null as bigint | null,
    loading: false,
  }),

  actions: {
    async fetchBalance(address: string) {
      this.loading = true
      try {
        const addr = Address.from(address)
        const balance = await provider.eth.getBalance({ address: addr })
        this.balances[address] = Wei.toEther(balance)
      } finally {
        this.loading = false
      }
    },

    async fetchBlockNumber() {
      this.blockNumber = await provider.eth.getBlockNumber()
    },
  },

  getters: {
    getBalance: (state) => (address: string) => state.balances[address],
  },
})
<script setup lang="ts">
import { useEthereumStore } from '@/stores/ethereum'

const store = useEthereumStore()
const address = '0x...'

store.fetchBalance(address)
</script>

<template>
  <p v-if="store.loading">Loading...</p>
  <p v-else>{{ store.getBalance(address) }} ETH</p>
</template>

Error Handling

Handle errors gracefully:
// composables/useEthereumQuery.ts
import { ref, type Ref } from 'vue'

interface QueryOptions<T> {
  onError?: (error: Error) => void
  retry?: number
}

export function useEthereumQuery<T>(
  fetcher: () => Promise<T>,
  options: QueryOptions<T> = {}
) {
  const data = ref<T | null>(null) as Ref<T | null>
  const loading = ref(true)
  const error = ref<Error | null>(null)

  async function execute(retries = options.retry ?? 0) {
    loading.value = true
    error.value = null
    try {
      data.value = await fetcher()
    } catch (e) {
      if (retries > 0) {
        await new Promise(r => setTimeout(r, 1000))
        return execute(retries - 1)
      }
      error.value = e as Error
      options.onError?.(e as Error)
    } finally {
      loading.value = false
    }
  }

  execute()

  return { data, loading, error, refresh: () => execute(options.retry) }
}

Next Steps