EIPs
EIP-6963 — Multi-injected provider discovery
Before 6963, dapps assumed there was one wallet, at window.ethereum. With multiple wallets installed, that broke. EIP-6963 specifies a window.dispatchEvent / window.addEventListener protocol: each wallet announces itself with a ProviderDetail, and the dapp picks which to use.
import {
discover_providers,
pick_provider,
} from "@ethernauta/eip/6963";
const providers = await discover_providers();
// → EIP6963ProviderDetail[]
const choice = await pick_provider("io.ethernauta");
// → EIP6963ProviderDetail | undefined Surface
Discovery
| Export | Signature | Purpose |
|---|---|---|
discover_providers | (options?) => Promise<EIP6963ProviderDetail[]> | Returns every wallet that announces within the discovery window. |
pick_provider | (rdns: string, options?) => Promise<EIP6963ProviderDetail \| undefined> | Run discovery and return the first match for the given rdns. |
announce | wallet-side | Dispatch a provider announcement (called by wallets, not dapps). |
DiscoverOptions:
{ target?: EventTarget; ms?: number } target— defaults towindow. Override for non-browser hosts or test mocks.ms— discovery window in milliseconds; defaults to 100.
Storage (persistent selection)
The library ships a small storage layer so a dapp can remember the user’s chosen wallet across reloads. Every storage operation is keyed:
import {
set_provider_detail,
get_provider_detail,
clear_provider_detail,
web_storage,
} from "@ethernauta/eip/6963";
const store = web_storage(localStorage);
const key = "wallet";
// after the user picks
set_provider_detail({ store, key, provider_detail: choice });
// later — rehydrate
const persisted = await get_provider_detail({ store, key });
// disconnect
clear_provider_detail({ store, key }); | Export | Signature | Purpose |
|---|---|---|
set_provider_detail | ({ store, key, provider_detail }) => void | Persist the chosen detail’s rdns. |
get_provider_detail | ({ store, key, target?, ms? }) => Promise<EIP6963ProviderDetail \| null> | Re-issue discovery, return the wallet matching the persisted rdns. |
clear_provider_detail | ({ store, key }) => void | Forget the persisted choice. |
web_storage | (storage) => Store | Adapter wrapping localStorage / sessionStorage. |
get_provider_detail is async because it re-runs the announce dance — only the rdns is stored, never the live Provider object. The Provider is always fresh.
Constants and types
| Export | Value / Purpose |
|---|---|
ANNOUNCE_EVENT | "eip6963:announceProvider" |
REQUEST_EVENT | "eip6963:requestProvider" |
EIP6963ProviderInfo | { rdns, uuid, name, icon } |
EIP6963ProviderDetail | { info, provider } |
EIP6963AnnounceProviderEvent | CustomEvent<EIP6963ProviderDetail> |
EIP6963RequestProviderEvent | CustomEvent<void> |
Store, DiscoverOptions, SetProviderDetailOptions, GetProviderDetailOptions, ClearProviderDetailOptions | option shapes |
All with matching Valibot schemas.
The dapp pattern
import {
get_provider_detail,
web_storage,
} from "@ethernauta/eip/6963";
const store = web_storage(localStorage);
const persisted = await get_provider_detail({ store, key: "wallet" });
if (persisted) {
// their previous pick is still installed — use it
use(persisted.provider);
} else {
// either no pick yet, or the previously-picked wallet was uninstalled
// show wallet picker
} In React
@ethernauta/react’s useProvider composes useProviderDetail (which calls get_provider_detail) with create_provider (the resolver adapter) — so React callers get the wrapped resolver directly:
import { useProvider } from "@ethernauta/react";
const provider = useProvider({ key: "wallet" });
// → { reader, signer, provider_detail } | null See @ethernauta/react for the full hook surface.
See also
- EIP-1193 — what each announced provider conforms to.
- @ethernauta/react → useProvider.
- Guide → multi-wallet via EIP-6963.