Multi Injected Provider Discovery - wallets announce themselves, dapps discover them.
Spec: https://eips.ethereum.org/EIPS/eip-6963 Reference: https://github.com/wevm/mipd
| Requirement | Implementation |
|---|---|
| Close to spec | Mirror EIP-6963 naming: ProviderInfo, ProviderDetail, uuid, rdns |
| Data-first | Branded types, namespace pattern, functions take data as first param |
| Cleanup | All subscriptions return () => void unsubscribe function |
| Deduplication | By uuid - same uuid updates existing, not duplicated |
| Immutability | Object.freeze() on all ProviderDetail/ProviderInfo |
| Environment | getPlatform() utility, descriptive errors for non-browser |
| No spam handling | Trusted environment assumption |
| Icon XSS | JSDoc warning only: "render via <img> to prevent XSS" |
// Branded, frozen
type ProviderInfo = Readonly<{
uuid: string; // UUIDv4
name: string; // Display name
icon: string; // Data URI, >=96x96px
rdns: string; // Reverse DNS (e.g., "io.metamask")
}> & { readonly [brand]: "ProviderInfo" };
type ProviderDetail = Readonly<{
info: ProviderInfo;
provider: EIP1193Provider;
}> & { readonly [brand]: "ProviderDetail" };// Subscribe to announcements, dedupes by uuid
const unsubscribe = EIP6963.subscribe(
(providers: ProviderDetail[]) => void
): () => void
// Snapshot of current providers
EIP6963.getProviders(): ProviderDetail[]
// Find specific provider
EIP6963.findProvider({ rdns: string }): ProviderDetail | undefined// Announce + auto re-announce on requestProvider events
const unsubscribe = EIP6963.announce({
info: ProviderInfoInput,
provider: EIP1193Provider,
}): () => void// Environment detection
EIP6963.getPlatform(): 'browser' | 'node' | 'bun' | 'worker' | 'unknown'
// Data constructors (validate + freeze)
EIP6963.ProviderInfo(input): ProviderInfo
EIP6963.ProviderDetail(input): ProviderDetailsrc/eip6963/
├── types.ts # ProviderInfo, ProviderDetail branded types
├── errors.ts # All error classes (EIP6963Error base + specifics)
├── validators.js # validateUuid, validateRdns, validateIcon, validateProvider
├── getPlatform.js # Detect browser/node/bun/worker + assertBrowser
├── ProviderInfo.js # Constructor: validate all fields, freeze
├── ProviderDetail.js # Constructor: validate info+provider, freeze
├── subscribe.js # Dapp: dispatch request, listen announces, dedupe
├── getProviders.js # Dapp: return current snapshot
├── findProvider.js # Dapp: find by rdns
├── announce.js # Wallet: announce + listen for requests
├── index.ts # Namespace exports
└── *.test.ts
Goal: Unblock examples and documentation before implementation.
Create all files with correct signatures, throw NotImplementedError:
// subscribe.js
export function subscribe(listener) {
throw new NotImplementedError('EIP6963.subscribe');
}- Overview of EIP-6963 purpose
- Dapp usage example (subscribe, getProviders, findProvider)
- Wallet usage example (announce)
- TypeScript types reference
- Environment requirements (browser only)
- Icon security note (JSDoc warning about XSS)
Create interactive example demonstrating:
- Discovering available wallets
- Displaying wallet info (name, icon)
- Connecting to selected provider
Create test files with pending tests:
// subscribe.test.ts
describe('EIP6963.subscribe', () => {
it.todo('calls listener with providers on announce');
it.todo('deduplicates by uuid');
it.todo('returns unsubscribe function');
it.todo('throws in non-browser environment');
});export function getPlatform() {
if (typeof window !== 'undefined') return 'browser';
if (typeof process !== 'undefined' && process.versions?.node) return 'node';
if (typeof Bun !== 'undefined') return 'bun';
if (typeof WorkerGlobalScope !== 'undefined') return 'worker';
return 'unknown';
}
export function assertBrowser() {
const platform = getPlatform();
if (platform !== 'browser') {
throw new UnsupportedEnvironmentError(
`EIP6963 requires browser. Detected: ${platform}`
);
}
}- Validate
uuidis UUIDv4 format - Validate
rdnsis valid reverse DNS Object.freeze()result- Brand with symbol
- Accept info + provider
- Freeze info via ProviderInfo()
- Freeze entire object
- Brand with symbol
// Internal state (module-scoped)
const providers = new Map(); // uuid -> ProviderDetail
const listeners = new Set();
export function subscribe(listener) {
assertBrowser();
// Add listener
listeners.add(listener);
// Set up announce listener (once globally)
if (listeners.size === 1) {
window.addEventListener('eip6963:announceProvider', handleAnnounce);
window.dispatchEvent(new Event('eip6963:requestProvider'));
}
// Emit current state immediately
listener([...providers.values()]);
// Return unsubscribe
return () => {
listeners.delete(listener);
if (listeners.size === 0) {
window.removeEventListener('eip6963:announceProvider', handleAnnounce);
}
};
}
function handleAnnounce(event) {
const detail = ProviderDetail(event.detail);
providers.set(detail.info.uuid, detail);
notify();
}
function notify() {
const snapshot = [...providers.values()];
listeners.forEach(l => l(snapshot));
}export function getProviders() {
return [...providers.values()];
}export function findProvider({ rdns }) {
for (const detail of providers.values()) {
if (detail.info.rdns === rdns) return detail;
}
return undefined;
}export function announce({ info, provider }) {
assertBrowser();
const detail = ProviderDetail({ info, provider });
const frozenDetail = Object.freeze(detail);
function emitAnnounce() {
window.dispatchEvent(
new CustomEvent('eip6963:announceProvider', { detail: frozenDetail })
);
}
// Initial announce
emitAnnounce();
// Re-announce on requests
window.addEventListener('eip6963:requestProvider', emitAnnounce);
// Return cleanup
return () => {
window.removeEventListener('eip6963:requestProvider', emitAnnounce);
};
}export { subscribe } from './subscribe.js';
export { getProviders } from './getProviders.js';
export { findProvider } from './findProvider.js';
export { announce } from './announce.js';
export { getPlatform } from './getPlatform.js';
export { ProviderInfo } from './ProviderInfo.js';
export { ProviderDetail } from './ProviderDetail.js';
export type { ProviderInfo, ProviderDetail } from './types.js';Goal: Verify an LLM can correctly use the API from docs alone.
Create test that:
- Provides LLM with only the docs/API reference
- Asks LLM to write code for specific task
- Executes generated code
- Validates correctness
// llm-test/eip6963.test.ts
const scenarios = [
{
prompt: 'Subscribe to wallet announcements and log each wallet name',
validate: (code) => {
// Must use subscribe()
// Must access detail.info.name
// Must handle unsubscribe
}
},
{
prompt: 'Find MetaMask provider by rdns and call eth_accounts',
validate: (code) => {
// Must use findProvider({ rdns: 'io.metamask' })
// Must call provider.request()
}
},
{
prompt: 'Announce a wallet with uuid, name, icon, rdns',
validate: (code) => {
// Must use announce()
// Must include all 4 info fields
// Must store unsubscribe for cleanup
}
}
];- LLM generates syntactically correct code
- Code uses correct API methods
- Code handles cleanup (unsubscribe)
- No hallucinated methods
| File | Phase |
|---|---|
src/eip6963/types.ts |
1 |
src/eip6963/index.ts |
1 |
src/eip6963/errors.ts |
1 |
src/eip6963/*.js (stubs) |
1 |
docs/eip6963.mdx |
1 |
examples/eip6963-discover.ts |
1 |
src/eip6963/*.test.ts |
1 |
src/eip6963/*.js (impl) |
2 |
llm-test/eip6963.test.ts |
3 |
| Error | Condition | Message |
|---|---|---|
UnsupportedEnvironmentError |
getPlatform() !== 'browser' | EIP6963 requires browser. Detected: ${platform} |
UnsupportedEnvironmentError |
window.dispatchEvent undefined | EIP6963 requires window.dispatchEvent |
| Error | Condition | Message |
|---|---|---|
InvalidUuidError |
uuid not UUIDv4 format | Invalid uuid: expected UUIDv4 format, got "${uuid}" |
InvalidRdnsError |
rdns not valid reverse DNS | Invalid rdns: expected reverse DNS format (e.g., "io.metamask"), got "${rdns}" |
MissingFieldError |
uuid/name/icon/rdns missing | ProviderInfo missing required field: ${field} |
InvalidFieldError |
field is empty string | ProviderInfo.${field} cannot be empty |
InvalidIconError |
icon not data URI | Invalid icon: expected data URI (data:image/...), got "${icon.slice(0,30)}..." |
| Error | Condition | Message |
|---|---|---|
MissingFieldError |
info missing | ProviderDetail missing required field: info |
MissingFieldError |
provider missing | ProviderDetail missing required field: provider |
InvalidProviderError |
provider.request not function | Invalid provider: expected EIP1193 provider with request() method |
| Error | Condition | Message |
|---|---|---|
InvalidArgumentError |
subscribe() listener not function | subscribe() requires function, got ${typeof listener} |
InvalidArgumentError |
findProvider() missing rdns | findProvider() requires { rdns: string } |
InvalidArgumentError |
announce() missing info/provider | announce() requires { info, provider } |
| Situation | Behavior |
|---|---|
| Malformed announceProvider event | Log warning, skip (don't crash dapp) |
| Duplicate uuid announcement | Update existing entry silently |
| Provider becomes unresponsive | Not our problem (user handles provider errors) |
// errors.ts
export class EIP6963Error extends Error {
readonly code: string;
}
export class UnsupportedEnvironmentError extends EIP6963Error {
readonly code = 'UNSUPPORTED_ENVIRONMENT';
readonly platform: string;
}
export class InvalidUuidError extends EIP6963Error {
readonly code = 'INVALID_UUID';
readonly uuid: string;
}
export class InvalidRdnsError extends EIP6963Error {
readonly code = 'INVALID_RDNS';
readonly rdns: string;
}
export class MissingFieldError extends EIP6963Error {
readonly code = 'MISSING_FIELD';
readonly field: string;
}
export class InvalidFieldError extends EIP6963Error {
readonly code = 'INVALID_FIELD';
readonly field: string;
}
export class InvalidIconError extends EIP6963Error {
readonly code = 'INVALID_ICON';
}
export class InvalidProviderError extends EIP6963Error {
readonly code = 'INVALID_PROVIDER';
}
export class InvalidArgumentError extends EIP6963Error {
readonly code = 'INVALID_ARGUMENT';
readonly argument: string;
}// validators.js
const UUID_V4_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
const RDNS_REGEX = /^[a-z0-9]+(\.[a-z0-9]+)+$/i;
const DATA_URI_REGEX = /^data:image\/(png|jpeg|gif|webp|svg\+xml);base64,/;
export function validateUuid(uuid) {
if (!uuid) throw new MissingFieldError('uuid');
if (typeof uuid !== 'string') throw new InvalidFieldError('uuid');
if (!UUID_V4_REGEX.test(uuid)) throw new InvalidUuidError(uuid);
}
export function validateRdns(rdns) {
if (!rdns) throw new MissingFieldError('rdns');
if (typeof rdns !== 'string') throw new InvalidFieldError('rdns');
if (!RDNS_REGEX.test(rdns)) throw new InvalidRdnsError(rdns);
}
export function validateIcon(icon) {
if (!icon) throw new MissingFieldError('icon');
if (typeof icon !== 'string') throw new InvalidFieldError('icon');
if (!DATA_URI_REGEX.test(icon)) throw new InvalidIconError(icon);
}
export function validateProvider(provider) {
if (!provider) throw new MissingFieldError('provider');
if (typeof provider.request !== 'function') {
throw new InvalidProviderError();
}
}- All methods return unsubscribe function (not AbortSignal)
- Objects frozen with Object.freeze()
- Deduplication by uuid works
- getPlatform() detects all environments (browser/node/bun/worker)
- Types mirror EIP-6963 spec exactly
- UnsupportedEnvironmentError thrown in non-browser with platform name
- InvalidUuidError for malformed UUIDs
- InvalidRdnsError for malformed reverse DNS
- MissingFieldError for missing required fields
- InvalidProviderError when provider.request not function
- InvalidArgumentError for bad function arguments
- Malformed announce events logged + skipped (no crash)
- All errors have
codeproperty for programmatic handling - All errors have descriptive messages with actual values
- JSDoc warns about icon XSS
- Tests cover all error conditions
- Tests cover happy paths
- Docs include dapp + wallet examples
- LLM can use API from docs alone