Concepts

1193 is a transport, not a policy

EIP-1193 defines a four-field interface:

type Provider = {
  request(args: RequestArguments): Promise<unknown>
  on(event, listener): void
  removeListener(event, listener): void
  emit(event, payload): void
}

That’s it. Method existence, routing, state caching, confirmation policy — none of those are part of 1193. They’re wallet-side concerns. Ethernauta enforces that separation rigidly (M5 in the project maxims).

Wallet side: a thin facade

The wallet’s create_envelope (in @ethernauta/eip/1193) produces the four-field 1193 object and nothing else. The dispatcher (packages/wallet/src/utils/dispatch.ts) routes incoming requests against four strict allowlists:

  • Wallet-state methods — answered from cache (eth_chainId, eth_accounts, net_version, wallet_getCapabilities, wallet_getPermissions, wallet_switchEthereumChain).
  • Chain-read methods — forwarded to the active chain’s RPC (eth_blockNumber, eth_call, eth_getBalance, …).
  • Signable methods — forwarded to the popup for user confirmation (eth_sendTransaction, personal_sign, eth_signTypedData_v4, wallet_sendCalls, wallet_sendSetCodeTransaction, …).
  • Wallet-internal methods — answered from extension storage (wallet_getCallsStatus for EIP-5792 batch tracking).

A method outside all four returns 4200 (Unsupported Method). The 1193 envelope itself doesn’t know any of this — it just calls a dispatch function.

Dapp side: the same shape

A dapp consuming a 1193 provider doesn’t reach for a different API. The library adapts the provider into the same resolver shapes:

import { create_provider } from "@ethernauta/transport";

const provider = create_provider(window.ethereum);

// for reads
const block = await eth_block_number()(
  provider.reader({ chain_id: 1 }),
);

// for signatures
const hash = await eth_send_transaction({ to, value })(
  provider.signer({ chain_id: 1 }),
);

The call shape is identical to create_reader(CHAINS) / create_signer(CHAINS). The only difference is the transport-construction line — which is the dapp’s decision, made per call, not a contrast every demo has to enact.

What this buys

Wallet-portable code. A dapp written against Ethernauta resolvers works against window.ethereum, any EIP-6963 announcement, a test mock, or the public chain reader for non-signing operations. The call sites don’t change.

Auditability. When the wallet handles wallet_sendCalls, the implementation is a 30-line function that composes wallet_getCapabilities, eth_signTransaction, and eth_sendRawTransaction. There is no hidden flow.

Standards-first interop. The Ethernauta wallet can serve any standards-compliant dapp; an Ethernauta dapp can talk to any standards-compliant wallet. The 1193 envelope is what makes that symmetric.

What 1193 doesn’t define

Things you might expect to be in the spec but aren’t:

  • Method allowlists. Each wallet decides which methods it implements. Ethernauta enforces this with four tier-shaped allowlists; other wallets do it differently.
  • State caching. eth_chainId could be a fresh RPC call every time or a cached value. The wallet picks.
  • Confirmation flow. When eth_sendTransaction arrives, the wallet decides whether to open a popup, autopilot via a saved policy, or reject outright.
  • Event semantics. accountsChanged fires when the user picks a different account; chainChanged fires on switch. But when exactly (before or after the related promise settles?) is implementation-defined.

All of those live in packages/wallet/, not in @ethernauta/eip/1193. The 1193 package is the protocol envelope; the wallet package is the policy.

EIP-6963 sits on top

Discovery (which provider, when there might be many) is a separate spec — EIP-6963. The wallet announces itself; dapps listen. The four-field 1193 envelope is the same regardless of how the dapp got hold of it.

See @ethernauta/eip/6963 for the discovery surface.