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 —
emitis 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.
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):
| Event | Payload | When wallets fire it |
|---|---|---|
accountsChanged | string[] (addresses) | User picks a different account, disconnects, connects |
chainChanged | string (hex chain id) | User switches to a different chain |
connect | { chainId: string } | Provider becomes ready |
disconnect | ProviderRpcError | Provider loses its connection |
message | EthSubscription / generic | Subscription 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.