Skip to content

Instantly share code, notes, and snippets.

@roninjin10
Created December 29, 2025 09:18
Show Gist options
  • Select an option

  • Save roninjin10/114314ae2417f0f8f54044021741afd4 to your computer and use it in GitHub Desktop.

Select an option

Save roninjin10/114314ae2417f0f8f54044021741afd4 to your computer and use it in GitHub Desktop.
Prompt example

EIP-6963 Implementation Spec

Overview

Multi Injected Provider Discovery - wallets announce themselves, dapps discover them.

Spec: https://eips.ethereum.org/EIPS/eip-6963 Reference: https://github.com/wevm/mipd


Core Requirements

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"

Data Types

// 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" };

API Surface

Dapp Side (Consumer)

// 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

Wallet Side (Producer)

// Announce + auto re-announce on requestProvider events
const unsubscribe = EIP6963.announce({
  info: ProviderInfoInput,
  provider: EIP1193Provider,
}): () => void

Utilities

// Environment detection
EIP6963.getPlatform(): 'browser' | 'node' | 'bun' | 'worker' | 'unknown'

// Data constructors (validate + freeze)
EIP6963.ProviderInfo(input): ProviderInfo
EIP6963.ProviderDetail(input): ProviderDetail

File Structure

src/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

Phase 1: API Stubs + Docs + Playground

Goal: Unblock examples and documentation before implementation.

1.1 API Stubs

Create all files with correct signatures, throw NotImplementedError:

// subscribe.js
export function subscribe(listener) {
  throw new NotImplementedError('EIP6963.subscribe');
}

1.2 Documentation (docs/eip6963.mdx)

  • 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)

1.3 Playground Example

Create interactive example demonstrating:

  • Discovering available wallets
  • Displaying wallet info (name, icon)
  • Connecting to selected provider

1.4 Test Structure

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');
});

Phase 2: Implementation

2.1 getPlatform.js

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}`
    );
  }
}

2.2 ProviderInfo.js

  • Validate uuid is UUIDv4 format
  • Validate rdns is valid reverse DNS
  • Object.freeze() result
  • Brand with symbol

2.3 ProviderDetail.js

  • Accept info + provider
  • Freeze info via ProviderInfo()
  • Freeze entire object
  • Brand with symbol

2.4 subscribe.js

// 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));
}

2.5 getProviders.js

export function getProviders() {
  return [...providers.values()];
}

2.6 findProvider.js

export function findProvider({ rdns }) {
  for (const detail of providers.values()) {
    if (detail.info.rdns === rdns) return detail;
  }
  return undefined;
}

2.7 announce.js

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);
  };
}

2.8 index.ts

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';

Phase 3: LLM One-Shot Testing

Goal: Verify an LLM can correctly use the API from docs alone.

3.1 Test Setup

Create test that:

  1. Provides LLM with only the docs/API reference
  2. Asks LLM to write code for specific task
  3. Executes generated code
  4. Validates correctness

3.2 Test Cases

// 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
    }
  }
];

3.3 Success Criteria

  • LLM generates syntactically correct code
  • Code uses correct API methods
  • Code handles cleanup (unsubscribe)
  • No hallucinated methods

Critical Files to Create

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 Conditions

Environment Errors

Error Condition Message
UnsupportedEnvironmentError getPlatform() !== 'browser' EIP6963 requires browser. Detected: ${platform}
UnsupportedEnvironmentError window.dispatchEvent undefined EIP6963 requires window.dispatchEvent

Validation Errors (ProviderInfo)

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)}..."

Validation Errors (ProviderDetail)

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

Runtime Errors

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 }

Defensive Handling (no throw, just ignore)

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)

Error Class Hierarchy

// 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;
}

Validation Helpers

// 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();
  }
}

Validation Checklist

Core Behavior

  • 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

Error Handling

  • 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 code property for programmatic handling
  • All errors have descriptive messages with actual values

Docs & Tests

  • 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment