Guides

Migrating from ethers / viem

What the same operations look like across the three libraries. This guide is a translation reference — not an argument that you should switch — though the third column will, in places, make the case on its own.

Setup

ethersviemethernauta
new JsonRpcProvider(url)createPublicClient({ chain, transport: http() })create_reader([eip155_1])
new Web3Provider(window.ethereum)createWalletClient({ chain, transport: custom(window.ethereum) })create_provider(window.ethereum).signer({ chain_id: "eip155:1" })

Reading a block

import { JsonRpcProvider } from "ethers";
import { createPublicClient, http as viem_http } from "viem";
import { mainnet } from "viem/chains";
import { eth_blockNumber } from "@ethernauta/eth";
import { create_reader, encode_chain_id, http } from "@ethernauta/transport";
import { eip155_1 } from "@ethernauta/chain/eip155-1";

const CHAIN_ID = encode_chain_id({ namespace: "eip155", reference: eip155_1.chainId });
const reader = create_reader([
  { chainId: CHAIN_ID, transports: [http("https://ethereum-rpc.publicnode.com")] },
]);

// ethers
const ethers_provider = new JsonRpcProvider("https://ethereum-rpc.publicnode.com");
const ethers_block = await ethers_provider.getBlockNumber();
void ethers_block;

// viem
const publicClient = createPublicClient({ chain: mainnet, transport: viem_http() });
const viem_block = await publicClient.getBlockNumber();
void viem_block;

// ethernauta
const block = await eth_blockNumber()(reader({ chain_id: CHAIN_ID }));
void block;

Reading a balance

import { JsonRpcProvider } from "ethers";
import { createPublicClient, http as viem_http } from "viem";
import { mainnet } from "viem/chains";
import { AddressSchema } from "@ethernauta/core";
import { eth_getBalance } from "@ethernauta/eth";
import { create_reader, encode_chain_id, http } from "@ethernauta/transport";
import { eip155_1 } from "@ethernauta/chain/eip155-1";
import { parse } from "valibot";

const address = parse(AddressSchema, "0x70997970C51812dc3A010C7d01b50e0d17dc79C8");
const CHAIN_ID = encode_chain_id({ namespace: "eip155", reference: eip155_1.chainId });
const reader = create_reader([
  { chainId: CHAIN_ID, transports: [http("https://ethereum-rpc.publicnode.com")] },
]);

// ethers
const ethers_provider = new JsonRpcProvider("https://ethereum-rpc.publicnode.com");
const ethers_balance = await ethers_provider.getBalance(address);
void ethers_balance;

// viem
const publicClient = createPublicClient({ chain: mainnet, transport: viem_http() });
const viem_balance = await publicClient.getBalance({ address });
void viem_balance;

// ethernauta
const balance = await eth_getBalance([address, "latest"])(
  reader({ chain_id: CHAIN_ID }),
);
void balance;

Sending a transaction (path 1)

import type { Wallet } from "ethers";
import type { WalletClient } from "viem";
import { AddressSchema, BytesSchema, UintSchema } from "@ethernauta/core";
import { eth_sendTransaction } from "@ethernauta/eth";
import type { Provider } from "@ethernauta/eip/1193";
import { create_provider, encode_chain_id } from "@ethernauta/transport";
import { eip155_1 } from "@ethernauta/chain/eip155-1";
import { parse } from "valibot";

declare const ethers_signer: Wallet;
declare const walletClient: WalletClient;

const to = parse(AddressSchema, "0x70997970C51812dc3A010C7d01b50e0d17dc79C8");
const value = parse(UintSchema, "0x16345785D8A0000");
const input = parse(BytesSchema, "0x");
const CHAIN_ID = encode_chain_id({ namespace: "eip155", reference: eip155_1.chainId });
declare const provider: Provider; // see /eips/6963 for discovery
const { signer } = create_provider(provider);

// ethers
const ethers_tx = await ethers_signer.sendTransaction({ to, value });
void ethers_tx;

// viem — call shape sketch
void walletClient;

// ethernauta
const hash = await eth_sendTransaction([{ to, value, input }])(signer({ chain_id: CHAIN_ID }));
void hash;

Sending a transaction (path 2 — separate sign and broadcast)

import type { Wallet, JsonRpcProvider } from "ethers";
import { AddressSchema, BytesSchema, UintSchema } from "@ethernauta/core";
import { eth_sendRawTransaction, eth_signTransaction } from "@ethernauta/eth";
import type { Provider } from "@ethernauta/eip/1193";
import { create_provider, create_writer, encode_chain_id, http } from "@ethernauta/transport";
import { eip155_1 } from "@ethernauta/chain/eip155-1";
import { parse } from "valibot";

declare const ethers_signer: Wallet;
declare const ethers_provider: JsonRpcProvider;

const to = parse(AddressSchema, "0x70997970C51812dc3A010C7d01b50e0d17dc79C8");
const value = parse(UintSchema, "0x16345785D8A0000");
const input = parse(BytesSchema, "0x");
const CHAIN_ID = encode_chain_id({ namespace: "eip155", reference: eip155_1.chainId });
declare const provider: Provider; // see /eips/6963 for discovery
const { signer } = create_provider(provider);
const writer = create_writer([
  { chainId: CHAIN_ID, transports: [http("https://ethereum-rpc.publicnode.com")] },
]);

// ethers — not first-class; requires raw signer access
const ethers_signed = await ethers_signer.signTransaction({ to, value });
const ethers_tx = await ethers_provider.broadcastTransaction(ethers_signed);
void ethers_tx;

// viem — not first-class; eth_signTransaction is feature-flagged
// (most providers don't expose it without configuration)

// ethernauta — first-class, no flags needed
const signed = await eth_signTransaction([{ to, value, input }])(signer({ chain_id: CHAIN_ID }));
const hash = await eth_sendRawTransaction([signed])(writer({ chain_id: CHAIN_ID }));
void hash;

This is the primary differentiator — see Concepts → two paths.

Calling a contract

import { Contract, JsonRpcProvider } from "ethers";
import { createPublicClient, http as viem_http, parseAbi } from "viem";
import { mainnet } from "viem/chains";
import type { Bytes } from "@ethernauta/core";
import { AddressSchema } from "@ethernauta/core";
import { balanceOf } from "@ethernauta/erc/20";
import { eth_call } from "@ethernauta/eth";
import { contract, create_reader, encode_chain_id, http } from "@ethernauta/transport";
import { eip155_1 } from "@ethernauta/chain/eip155-1";
import { parse } from "valibot";

const address = parse(AddressSchema, "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
const holder = parse(AddressSchema, "0x70997970C51812dc3A010C7d01b50e0d17dc79C8");
const CHAIN_ID = encode_chain_id({ namespace: "eip155", reference: eip155_1.chainId });
const reader = create_reader([
  { chainId: CHAIN_ID, transports: [http("https://ethereum-rpc.publicnode.com")] },
]);

// ethers
const abi = ["function balanceOf(address) view returns (uint256)"];
const ethers_provider = new JsonRpcProvider("https://ethereum-rpc.publicnode.com");
const ethers_contract = new Contract(address, abi, ethers_provider);
const ethers_balance = await ethers_contract.balanceOf(holder);
void ethers_balance;

// viem
const publicClient = createPublicClient({ chain: mainnet, transport: viem_http() });
const viem_balance = await publicClient.readContract({
  address,
  abi: parseAbi(["function balanceOf(address) view returns (uint256)"]),
  functionName: "balanceOf",
  args: [holder],
});
void viem_balance;

// ethernauta
const callable = balanceOf([holder])(contract({ chain_id: CHAIN_ID, to: address }));
const result_bytes: Bytes = await eth_call([{ to: callable.to, input: callable.data }])(
  reader({ chain_id: CHAIN_ID }),
);
const balance = callable.decode(result_bytes);
void balance;

balanceOf comes from @ethernauta/erc/20 — no need to wire an ABI fragment for standard interfaces. For custom contracts, use make_codec + eth_call.

EIP-712 typed-data

import type { Wallet } from "ethers";
import type { WalletClient } from "viem";
import type { Address } from "@ethernauta/core";
import { AddressSchema } from "@ethernauta/core";
import { eth_signTypedData_v4, type TypedData } from "@ethernauta/eip/712";
import type { Provider } from "@ethernauta/eip/1193";
import { create_provider, encode_chain_id } from "@ethernauta/transport";
import { eip155_1 } from "@ethernauta/chain/eip155-1";
import { parse } from "valibot";

declare const ethers_signer: Wallet;
declare const walletClient: WalletClient;
declare const typed_data: TypedData;
const account: Address = parse(AddressSchema, "0x70997970C51812dc3A010C7d01b50e0d17dc79C8");

const CHAIN_ID = encode_chain_id({ namespace: "eip155", reference: eip155_1.chainId });
declare const provider: Provider; // see /eips/6963 for discovery
const { signer } = create_provider(provider);

// ethers
const ethers_sig = await ethers_signer.signTypedData(
  typed_data.domain,
  typed_data.types,
  typed_data.message,
);
void ethers_sig;

// viem — call shape sketch
void walletClient;

// ethernauta
const signature = await eth_signTypedData_v4([account, typed_data])(signer({ chain_id: CHAIN_ID }));
void signature;

Listening for events

import type { WebSocketProvider } from "ethers";
import type { PublicClient } from "viem";
import { eth_subscribeNewHeads } from "@ethernauta/eth";
import { create_subscriber, encode_chain_id, websocket } from "@ethernauta/transport";
import { eip155_1 } from "@ethernauta/chain/eip155-1";

declare const ethers_provider: WebSocketProvider;
declare const publicClient: PublicClient;

// ethers
ethers_provider.on("block", (block: number) => { void block; });

// viem
publicClient.watchBlockNumber({ onBlockNumber: (b: bigint) => { void b; } });

// ethernauta (WebSocket transport)
const CHAIN_ID = encode_chain_id({ namespace: "eip155", reference: eip155_1.chainId });
const subscriber = create_subscriber([
  { chainId: CHAIN_ID, websockets: [websocket("wss://ethereum-rpc.publicnode.com")] },
]);
const unsubscribe = await eth_subscribeNewHeads()(
  subscriber({ chain_id: CHAIN_ID }),
  (block) => { void block; },
);
void unsubscribe;

The Ethernauta version requires a WebSocket transport (subscriptions can’t run over HTTP).

The mental model swap

What you do in ethers/viemWhat you do in Ethernauta
Pick a client kind (PublicClient, WalletClient, …)Pick a resolver shape (Readable, Writable, Signable, Callable)
Call methods on the clientCurry: method(args)(resolver(...))
Pass an ABI alongside the callUse the per-ERC binding, or make_codec for custom
Choose between client kinds at constructionCompose any shape per call
One way to send transactionsChoose between path 1 and path 2 per call

See also