Getting Started

First read

A chain read. No wallet required. Ten lines.

import { create_reader } from "@ethernauta/transport";
import { eth_block_number, eth_get_balance } from "@ethernauta/eth";
import { eip155_1, eip155_11155111 } from "@ethernauta/chain";

const reader = create_reader([eip155_1, eip155_11155111]);

const number = await eth_block_number()(
  reader({ chain_id: eip155_1.chain_id }),
);

const balance = await eth_get_balance({
  address: "0xd8dA6BF26964aF9D7eED9e03E53415D37aA96045",
  block: "latest",
})(reader({ chain_id: eip155_1.chain_id }));

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_block_number() — 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_block_number 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

const mainnet_block = await eth_block_number()(
  reader({ chain_id: eip155_1.chain_id }),
);

const sepolia_block = await eth_block_number()(
  reader({ chain_id: eip155_11155111.chain_id }),
);

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