Skip to content
LogoLogo

Transaction status

Each intercepted write gets a stable id — a 4-byte identifier appended to the calldata as dataSuffix — and emits a sequence of TxStatus events through subscribeTx.

Lifecycle

statusHas hash?Meaning
simulatingnoThe call is running on the local Tevm fork.
optimisticnoThe local run succeeded and its Store events are in the optimistic overlay.
optimisticyesThe real transaction was broadcast; the hash is known.
confirmedyesThe receipt came back with status: 'success'.
revertedyesThe receipt came back reverted; the optimistic transaction is dropped from the pool and the overlay rolls back.

Note that optimistic is emitted twice: once when the simulation lands, and once again with the broadcast hash. Key your UI state on status.id, and treat each event as the latest state for that id.

Subscribing

txStatus.ts
import type { TxStatus } from '@tevm/mud'
import { optimistic } from './optimistic'
 
const pending = new Map<string, TxStatus>()
 
const unsubscribe = optimistic.subscribeTx({
	subscriber: (status) => {
		if (status.status === 'confirmed' || status.status === 'reverted') {
			pending.delete(status.id)
		} else {
			pending.set(status.id, status)
		}
 
		console.log(
			`[${status.id}] ${status.status}`,
			'hash' in status ? status.hash : '(local only)',
			new Date(status.timestamp).toISOString(),
		)
	},
})
 
// Stop listening
unsubscribe()

A pending-transaction indicator in React

PendingTxs.tsx
import { useEffect, useMemo, useState } from 'react'
import { useOptimisticWrapper } from '@tevm/mud/react'
import type { TxStatus } from '@tevm/mud'
 
export function PendingTxs() {
	const wrapper = useOptimisticWrapper()
	const [byId, setById] = useState<Record<string, TxStatus>>({})
 
	useEffect(() => {
		if (!wrapper) return
		return wrapper.subscribeTx({
			subscriber: (status) =>
				setById((prev) => ({
					...prev,
					[status.id]: status,
				})),
		})
	}, [wrapper])
 
	const pending = useMemo(
		() => Object.values(byId).filter((tx) => tx.status === 'simulating' || tx.status === 'optimistic'),
		[byId],
	)
 
	if (pending.length === 0) return null
 
	return (
		<div role="status">
			{pending.length} pending {pending.length === 1 ? 'move' : 'moves'}
		</div>
	)
}

Handling reverts

A revert on chain is the one case where the player sees state snap back. When a receipt reports reverted, @tevm/mud removes the optimistic transaction from the Tevm txpool, which triggers a recomputation of the overlay from the remaining pool transactions — the bad write disappears and every subscriber is notified.

Surface it explicitly rather than letting the world silently rewind:

onRevert.ts
import { optimistic } from './optimistic'
 
optimistic.subscribeTx({
	subscriber: (status) => {
		if (status.status === 'reverted') {
			console.error(`Transaction ${status.hash} reverted; optimistic state rolled back.`)
		}
	},
})

Subscribing without a handler

subscribeTxStatus is exported standalone so you can build the same pattern over your own subscriber set — it is the primitive subscribeTx is built from.

standalone.ts
import { subscribeTxStatus, type TxStatus, type TxStatusSubscriber } from '@tevm/mud'
 
const subscribers = new Set<TxStatusSubscriber>()
const subscribe = subscribeTxStatus(subscribers)
 
const unsubscribe = subscribe((status: TxStatus) => console.log(status.status))
unsubscribe()