Getting Started
First read
A chain read. No wallet required. Ten lines.
import { parse } from "valibot";
import { AddressSchema } from "@ethernauta/core";
import { create_reader, encode_chain_id, http } from "@ethernauta/transport";
import { eth_blockNumber, eth_getBalance } from "@ethernauta/eth";
import { eip155_1 } from "@ethernauta/chain/eip155-1";
import { eip155_11155111 } from "@ethernauta/chain/eip155-11155111";
const MAINNET = encode_chain_id({ namespace: "eip155", reference: eip155_1.chainId });
const SEPOLIA = encode_chain_id({ namespace: "eip155", reference: eip155_11155111.chainId });
const reader = create_reader([
{ chainId: MAINNET, transports: [http("https://ethereum-rpc.publicnode.com")] },
{ chainId: SEPOLIA, transports: [http("https://ethereum-sepolia-rpc.publicnode.com")] },
]);
const number = await eth_blockNumber()(
reader({ chain_id: MAINNET }),
);
void number;
const vitalik = parse(AddressSchema, "0xd8dA6BF26964aF9D7eED9e03E53415D37aA96045");
const balance = await eth_getBalance([vitalik, "latest"])(
reader({ chain_id: MAINNET }),
);
void balance; What just happened
create_reader([chains]) — built a Reader factory that knows how to dial public RPC endpoints for any chain you pass in. The chain objects carry their own RPC URL list.
eth_blockNumber() — first call. Binds the method’s parameters (none, in this case). Returns a curried function with the shape (_resolved: ResolvedReader) => Promise<Uint>.
reader({ chain_id: eip155_1.chain_id }) — built a ResolvedReader for mainnet by picking the chain out of the registry.
The outer call — passes the resolved reader into the curried method. The HTTP request happens here.
The two-call shape — method(args)(resolver(...)) — is never collapsed. The first call binds parameters; the second binds the transport. That separation is what lets the same eth_blockNumber run against a public RPC reader, an EIP-1193 provider, or a test mock with zero changes at the call site.
Different chains, same call shape
import { create_reader, encode_chain_id, http } from "@ethernauta/transport";
import { eth_blockNumber } from "@ethernauta/eth";
import { eip155_1 } from "@ethernauta/chain/eip155-1";
import { eip155_11155111 } from "@ethernauta/chain/eip155-11155111";
const MAINNET = encode_chain_id({ namespace: "eip155", reference: eip155_1.chainId });
const SEPOLIA = encode_chain_id({ namespace: "eip155", reference: eip155_11155111.chainId });
const reader = create_reader([
{ chainId: MAINNET, transports: [http("https://ethereum-rpc.publicnode.com")] },
{ chainId: SEPOLIA, transports: [http("https://ethereum-sepolia-rpc.publicnode.com")] },
]);
const mainnet_block = await eth_blockNumber()(
reader({ chain_id: MAINNET }),
);
void mainnet_block;
const sepolia_block = await eth_blockNumber()(
reader({ chain_id: SEPOLIA }),
);
void sepolia_block; One reader, many chains. The chain_id argument picks the RPC at call time.
Where the URLs come from
The reader doesn’t hard-code endpoints. Each chain definition in @ethernauta/chain carries an rpc array of public endpoints. The HTTP transport rotates through them on failure. You can pass your own list if you want to point at a private RPC.
Next
- First signature — sign and broadcast.
- Reading from the chain — the full
eth_*read surface. - Concepts → resolver shapes — why
Readerexists at all.