Ethernauta

Standalone EIP-1193 emitter

create_emitter() is the in-memory primitive the Ethernauta wallet uses to build the on / removeListener / emit surface of its 1193 provider. Dapps almost never construct one directly — they consume the wallet's provider — but the primitive is exported so you can:

  • Build a test fixture provider that emits scripted events.
  • Compose your own 1193-shaped object in non-wallet code (a hosted relay, an injected adapter, an extension's in-page bridge).
  • Understand exactly what the event semantics are: no filtering, no batching, no async — emit is a sync fan out to every currently-registered listener.

A locally-constructed create_emitter() with two listeners attached. Click a button to fire an event; the listener pushes a row into the log below. No wallet, no chain — pure in-memory.

View on GitHub

The code

import "./demo.css"
// The standalone EIP-1193 event emitter — without a wallet, without a
// chain, without an HTTP roundtrip. `create_emitter()` is the in-memory
// primitive the wallet uses to build its `on / removeListener / emit`
// surface; this demo wires two listeners to it and lets you fire each
// of the standard 1193 events by hand.

import {
  create_emitter,
  type Emitter,
} from "@ethernauta/eip/1193"
import { useEffect, useMemo, useState } from "react"
import { Button } from "../../components/button"

type LogEntry = {
  event: "accountsChanged" | "chainChanged"
  payload: string
  at: string
}

const SAMPLE_ACCOUNTS_A: string[] = [
  "0xd8dA6BF26964aF9D7eED9e03E53415D37aA96045",
]
const SAMPLE_ACCOUNTS_B: string[] = [
  "0xd8dA6BF26964aF9D7eED9e03E53415D37aA96045",
  "0x1111111254EEB25477B68fb85Ed929f73A960582",
]
const EMPTY_ACCOUNTS: string[] = []

const CHAIN_MAINNET = "0x1"
const CHAIN_BASE = "0x2105"
const CHAIN_OP = "0xa"

function now_iso(): string {
  return new Date().toISOString().slice(11, 19)
}

export function EmitterDemo() {
  // useMemo so the emitter is created once per mount; useEffect
  // below subscribes and tears down with the component.
  const emitter: Emitter = useMemo(
    () => create_emitter(),
    [],
  )
  const [log, set_log] = useState<LogEntry[]>([])

  useEffect(() => {
    function on_accounts(accounts: string[]): void {
      set_log((prev) => [
        {
          event: "accountsChanged",
          payload: JSON.stringify(accounts),
          at: now_iso(),
        },
        ...prev,
      ])
    }
    function on_chain(chain_id: string): void {
      set_log((prev) => [
        {
          event: "chainChanged",
          payload: chain_id,
          at: now_iso(),
        },
        ...prev,
      ])
    }
    emitter.on("accountsChanged", on_accounts)
    emitter.on("chainChanged", on_chain)
    return () => {
      emitter.removeListener("accountsChanged", on_accounts)
      emitter.removeListener("chainChanged", on_chain)
    }
  }, [emitter])

  return (
    <div className="emitter-root">
      <p className="emitter-description">
        A locally-constructed <code>create_emitter()</code>{" "}
        with two listeners attached. Click a button to fire
        an event; the listener pushes a row into the log
        below. No wallet, no chain — pure in-memory.
      </p>
      <div className="emitter-buttons">
        <Button
          onClick={() => {
            emitter.emit(
              "accountsChanged",
              SAMPLE_ACCOUNTS_A,
            )
          }}
        >
          accountsChanged (1 account)
        </Button>
        <Button
          onClick={() => {
            emitter.emit(
              "accountsChanged",
              SAMPLE_ACCOUNTS_B,
            )
          }}
        >
          accountsChanged (2 accounts)
        </Button>
        <Button
          onClick={() => {
            emitter.emit("accountsChanged", EMPTY_ACCOUNTS)
          }}
        >
          accountsChanged ([])
        </Button>
        <Button
          onClick={() => {
            emitter.emit("chainChanged", CHAIN_MAINNET)
          }}
        >
          chainChanged → mainnet (0x1)
        </Button>
        <Button
          onClick={() => {
            emitter.emit("chainChanged", CHAIN_BASE)
          }}
        >
          chainChanged → Base (0x2105)
        </Button>
        <Button
          onClick={() => {
            emitter.emit("chainChanged", CHAIN_OP)
          }}
        >
          chainChanged → Optimism (0xa)
        </Button>
      </div>
      <Button
        onClick={() => {
          set_log([])
        }}
        disabled={log.length === 0}
      >
        Clear log
      </Button>
      {log.length > 0 && (
        <table className="emitter-log-table">
          <thead>
            <tr className="emitter-log-head">
              <th className="emitter-log-cell">time</th>
              <th className="emitter-log-cell">event</th>
              <th className="emitter-log-cell">payload</th>
            </tr>
          </thead>
          <tbody>
            {log.map((entry, i) => (
              <tr
                // biome-ignore lint/suspicious/noArrayIndexKey: display-only, prepended in order
                key={i}
                className="emitter-log-row"
              >
                <td className="emitter-log-cell">
                  {entry.at}
                </td>
                <td className="emitter-log-cell">
                  {entry.event}
                </td>
                <td className="emitter-log-payload">
                  {entry.payload}
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      )}
    </div>
  )
}

What the events mean

The five 1193-standard event names and their payload shapes (per EventMapSchema in packages/eip/src/1193/events.ts):

EventPayloadWhen wallets fire it
accountsChangedstring[] (addresses)User picks a different account, disconnects, connects
chainChangedstring (hex chain id)User switches to a different chain
connect{ chainId: string }Provider becomes ready
disconnectProviderRpcErrorProvider loses its connection
messageEthSubscription / genericSubscription push (rare for wallets)

This demo wires accountsChanged and chainChanged because they're the two events real dapps subscribe to. The others follow the same shape — fire them yourself in the console if you want to see them flow through the same emitter.

How dapps actually subscribe

Against a real wallet you don't touch the emitter directly — you call provider.on(event, listener) (or the watch_accounts / watch_chain helpers, which do the same with payload validation):

import { watch_accounts, watch_chain } from "@ethernauta/eip/1193"
import { discover_providers } from "@ethernauta/eip/6963"
 
const [detail] = await discover_providers()
if (!detail) throw new Error("no wallet")
 
const unsubscribe_accounts = watch_accounts(detail.provider, (accounts) => {
  console.log("accounts now:", accounts)
})
const unsubscribe_chain = watch_chain(detail.provider, (chain_id) => {
  console.log("chain now:", chain_id)
})
 
// later — tear down
unsubscribe_accounts()
unsubscribe_chain()

See the EIP-1193 provider demo for that flow against the actual wallet you have installed.