EIPs

EIP-6963 — Multi-injected provider discovery

eips.ethereum.org/EIPS/eip-6963

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

ExportSignaturePurpose
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.
announcewallet-sideDispatch a provider announcement (called by wallets, not dapps).

DiscoverOptions:

{ target?: EventTarget; ms?: number }
  • target — defaults to window. 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 });
ExportSignaturePurpose
set_provider_detail({ store, key, provider_detail }) => voidPersist 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 }) => voidForget the persisted choice.
web_storage(storage) => StoreAdapter 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

ExportValue / Purpose
ANNOUNCE_EVENT"eip6963:announceProvider"
REQUEST_EVENT"eip6963:requestProvider"
EIP6963ProviderInfo{ rdns, uuid, name, icon }
EIP6963ProviderDetail{ info, provider }
EIP6963AnnounceProviderEventCustomEvent<EIP6963ProviderDetail>
EIP6963RequestProviderEventCustomEvent<void>
Store, DiscoverOptions, SetProviderDetailOptions, GetProviderDetailOptions, ClearProviderDetailOptionsoption 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