Guides

Multi-wallet via EIP-6963

Before EIP-6963, dapps assumed window.ethereum was the wallet. With multiple wallets installed, that broke — whichever wallet ran last “wins” the window.ethereum slot. EIP-6963 specifies a discovery protocol: each wallet announces itself with metadata; dapps listen and pick.

Discovering providers

import { discover_providers } from "@ethernauta/eip/6963";

const providers = await discover_providers();
// → EIP6963ProviderDetail[]
// each carrying { info: { rdns, uuid, name, icon }, provider }

discover_providers dispatches the eip6963:requestProvider event and collects all announcements that arrive within a short window (default 100ms). By the time it resolves, every installed wallet has had a chance to announce.

Letting the user pick

The storage operations are keyed — the dapp owns the storage key, which lets one dapp persist multiple selections (e.g. a signer wallet and a viewer wallet) without collision.

import { set_provider_detail, web_storage } from "@ethernauta/eip/6963";

const store = web_storage(localStorage);
const key = "wallet";

for (const detail of providers) {
  render_button({
    icon: detail.info.icon,
    name: detail.info.name,
    onClick: () => {
      set_provider_detail({ store, key, provider_detail: detail });
      use(detail.provider);
    },
  });
}

set_provider_detail writes only the chosen wallet’s rdns — never the live Provider object (it can’t be serialized).

Rehydrating on reload

import { get_provider_detail } from "@ethernauta/eip/6963";

const persisted = await get_provider_detail({ store, key });
// → EIP6963ProviderDetail | null

get_provider_detail is async because it re-runs the discovery dance internally — the persisted rdns gets matched against the providers that announce on this page load. If the user uninstalled their previously-picked wallet, the result is null.

The returning-user pattern

import {
  get_provider_detail,
  discover_providers,
  web_storage,
} from "@ethernauta/eip/6963";

const store = web_storage(localStorage);
const key = "wallet";

const persisted = await get_provider_detail({ store, key });

if (persisted) {
  use(persisted.provider);
} else {
  // either first visit, or previously-picked wallet was uninstalled
  const providers = await discover_providers();
  show_wallet_picker(providers);
}

rdns is the canonical wallet ID (io.ethernauta, io.metamask, …) and what’s used for the persistent identity. name, uuid can drift.

Disconnecting

import { clear_provider_detail } from "@ethernauta/eip/6963";

clear_provider_detail({ store, key });

After this, get_provider_detail returns null until the user picks again.

In React

@ethernauta/react wraps this entire flow into hooks. useProvider({ key }) returns the already-wrapped resolver pair (reader, signer) bound to the persisted wallet — no need to call create_provider yourself:

import { useProvider } from "@ethernauta/react";

function App() {
  const provider = useProvider({ key: "wallet" });

  if (!provider) {
    return <WalletPicker />;
  }

  return <Dapp provider={provider} />;
}

See Guide → React integration for the picker UI and event subscriptions.

See also