provider.reader — exercise the read surface
Five eth_* read methods routed through the user's selected
EIP-1193 provider. Each button fires
method()(provider.reader({ chain_id })). The transport is
the wallet's RPC; the answers come from the wallet's
selected chain.
The call shape is identical to create_reader(CHAINS) — only
the line that builds the resolver changes.
Pick a wallet first (try the
View on GitHub
EIP-6963 example).The code
import "./demo.css"
// Each button below fires the same Readable method twice:
// `method()(provider.reader({ chain_id }))`. The transport
// is the user's selected EIP-1193 wallet — same call shape
// as a public-RPC reader, different transport-construction
// line.
import { AddressSchema } from "@ethernauta/core"
import {
eth_blockNumber,
eth_chainId,
eth_gasPrice,
eth_getBalance,
eth_maxPriorityFeePerGas,
} from "@ethernauta/eth"
import { useProvider } from "@ethernauta/react"
import { encode_chain_id } from "@ethernauta/transport"
import { useState } from "react"
import { parse } from "valibot"
import { Button } from "../../components/button"
import { SignInHint } from "../../components/sign-in-hint"
import { use_session } from "../../lib/auth/use-session"
import { PROVIDER_STORE_KEY } from "../../lib/provider-store"
// The injected provider answers from its own selected-chain
// state — this CAIP-2 id just satisfies the resolver context.
const DISCOVERY_CHAIN_ID = encode_chain_id({
namespace: "eip155",
reference: 1,
})
export function ProviderReadsDemo() {
const session = use_session()
const owner = session?.address ?? null
const provider = useProvider({ key: PROVIDER_STORE_KEY })
const [results, set_results] = useState<
Record<string, string>
>({})
const [errors, set_errors] = useState<
Record<string, string>
>({})
const [in_flight, set_in_flight] = useState<
string | null
>(null)
async function run(
method_name: string,
runner: () => Promise<string>,
) {
if (!provider) return
set_in_flight(method_name)
set_errors((prev) => {
const { [method_name]: _drop, ...rest } = prev
return rest
})
try {
const value = await runner()
set_results((prev) => ({
...prev,
[method_name]: value,
}))
} catch (e) {
set_errors((prev) => ({
...prev,
[method_name]:
e instanceof Error ? e.message : "Unknown error",
}))
} finally {
set_in_flight(null)
}
}
const reader = provider
? provider.reader({ chain_id: DISCOVERY_CHAIN_ID })
: null
if (!provider) {
return (
<div className="provider-reads-empty">
Pick a wallet first (try the <code>EIP-6963</code>{" "}
example).
</div>
)
}
return (
<div className="provider-reads-root">
<ReadRow
label="eth_blockNumber"
result={results.eth_blockNumber}
error={errors.eth_blockNumber}
loading={in_flight === "eth_blockNumber"}
disabled={in_flight !== null}
onClick={() =>
run("eth_blockNumber", async () => {
if (!reader) throw new Error("No provider")
return await eth_blockNumber()(reader)
})
}
/>
<ReadRow
label="eth_chainId"
result={results.eth_chainId}
error={errors.eth_chainId}
loading={in_flight === "eth_chainId"}
disabled={in_flight !== null}
onClick={() =>
run("eth_chainId", async () => {
if (!reader) throw new Error("No provider")
return await eth_chainId()(reader)
})
}
/>
<ReadRow
label="eth_gasPrice"
result={results.eth_gasPrice}
error={errors.eth_gasPrice}
loading={in_flight === "eth_gasPrice"}
disabled={in_flight !== null}
onClick={() =>
run("eth_gasPrice", async () => {
if (!reader) throw new Error("No provider")
return await eth_gasPrice()(reader)
})
}
/>
<ReadRow
label="eth_maxPriorityFeePerGas"
result={results.eth_maxPriorityFeePerGas}
error={errors.eth_maxPriorityFeePerGas}
loading={in_flight === "eth_maxPriorityFeePerGas"}
disabled={in_flight !== null}
onClick={() =>
run("eth_maxPriorityFeePerGas", async () => {
if (!reader) throw new Error("No provider")
return await eth_maxPriorityFeePerGas()(reader)
})
}
/>
<ReadRow
label="eth_getBalance (connected account)"
result={results.eth_getBalance}
error={errors.eth_getBalance}
loading={in_flight === "eth_getBalance"}
disabled={in_flight !== null || !owner}
onClick={() =>
run("eth_getBalance", async () => {
if (!reader) throw new Error("No provider")
if (!owner) throw new Error("Sign in first")
return await eth_getBalance([
parse(AddressSchema, owner),
"latest",
])(reader)
})
}
/>
{!owner && (
<div className="provider-reads-sign-in-hint">
<SignInHint />
</div>
)}
</div>
)
}
function ReadRow({
label,
result,
error,
loading,
disabled,
onClick,
}: {
label: string
result: string | undefined
error: string | undefined
loading: boolean
disabled: boolean
onClick: () => void
}) {
return (
<div className="provider-reads-row">
<div className="provider-reads-row-text">
<span className="provider-reads-row-label">
{label}
</span>
<span
className={
error
? "provider-reads-row-value is-error"
: "provider-reads-row-value"
}
>
{error ?? result ?? "—"}
</span>
</div>
<Button
variant="secondary"
onClick={onClick}
disabled={disabled}
>
{loading ? "…" : "Read"}
</Button>
</div>
)
}
The two paths, side by side
The same eth_blockNumber call works against a public RPC
with no wallet involved — just swap the transport:
import "./demo.css"
// Each button below fires the same Readable method twice:
// `method()(provider.reader({ chain_id }))`. The transport
// is the user's selected EIP-1193 wallet — same call shape
// as a public-RPC reader, different transport-construction
// line.
import { AddressSchema } from "@ethernauta/core"
import {
eth_blockNumber,
eth_chainId,
eth_gasPrice,
eth_getBalance,
eth_maxPriorityFeePerGas,
} from "@ethernauta/eth"
import { useProvider } from "@ethernauta/react"
import { encode_chain_id } from "@ethernauta/transport"
import { useState } from "react"
import { parse } from "valibot"
import { Button } from "../../components/button"
import { SignInHint } from "../../components/sign-in-hint"
import { use_session } from "../../lib/auth/use-session"
import { PROVIDER_STORE_KEY } from "../../lib/provider-store"
// The injected provider answers from its own selected-chain
// state — this CAIP-2 id just satisfies the resolver context.
const DISCOVERY_CHAIN_ID = encode_chain_id({
namespace: "eip155",
reference: 1,
})
export function ProviderReadsDemo() {
const session = use_session()
const owner = session?.address ?? null
const provider = useProvider({ key: PROVIDER_STORE_KEY })
const [results, set_results] = useState<
Record<string, string>
>({})
const [errors, set_errors] = useState<
Record<string, string>
>({})
const [in_flight, set_in_flight] = useState<
string | null
>(null)
async function run(
method_name: string,
runner: () => Promise<string>,
) {
if (!provider) return
set_in_flight(method_name)
set_errors((prev) => {
const { [method_name]: _drop, ...rest } = prev
return rest
})
try {
const value = await runner()
set_results((prev) => ({
...prev,
[method_name]: value,
}))
} catch (e) {
set_errors((prev) => ({
...prev,
[method_name]:
e instanceof Error ? e.message : "Unknown error",
}))
} finally {
set_in_flight(null)
}
}
const reader = provider
? provider.reader({ chain_id: DISCOVERY_CHAIN_ID })
: null
if (!provider) {
return (
<div className="provider-reads-empty">
Pick a wallet first (try the <code>EIP-6963</code>{" "}
example).
</div>
)
}
return (
<div className="provider-reads-root">
<ReadRow
label="eth_blockNumber"
result={results.eth_blockNumber}
error={errors.eth_blockNumber}
loading={in_flight === "eth_blockNumber"}
disabled={in_flight !== null}
onClick={() =>
run("eth_blockNumber", async () => {
if (!reader) throw new Error("No provider")
return await eth_blockNumber()(reader)
})
}
/>
<ReadRow
label="eth_chainId"
result={results.eth_chainId}
error={errors.eth_chainId}
loading={in_flight === "eth_chainId"}
disabled={in_flight !== null}
onClick={() =>
run("eth_chainId", async () => {
if (!reader) throw new Error("No provider")
return await eth_chainId()(reader)
})
}
/>
<ReadRow
label="eth_gasPrice"
result={results.eth_gasPrice}
error={errors.eth_gasPrice}
loading={in_flight === "eth_gasPrice"}
disabled={in_flight !== null}
onClick={() =>
run("eth_gasPrice", async () => {
if (!reader) throw new Error("No provider")
return await eth_gasPrice()(reader)
})
}
/>
<ReadRow
label="eth_maxPriorityFeePerGas"
result={results.eth_maxPriorityFeePerGas}
error={errors.eth_maxPriorityFeePerGas}
loading={in_flight === "eth_maxPriorityFeePerGas"}
disabled={in_flight !== null}
onClick={() =>
run("eth_maxPriorityFeePerGas", async () => {
if (!reader) throw new Error("No provider")
return await eth_maxPriorityFeePerGas()(reader)
})
}
/>
<ReadRow
label="eth_getBalance (connected account)"
result={results.eth_getBalance}
error={errors.eth_getBalance}
loading={in_flight === "eth_getBalance"}
disabled={in_flight !== null || !owner}
onClick={() =>
run("eth_getBalance", async () => {
if (!reader) throw new Error("No provider")
if (!owner) throw new Error("Sign in first")
return await eth_getBalance([
parse(AddressSchema, owner),
"latest",
])(reader)
})
}
/>
{!owner && (
<div className="provider-reads-sign-in-hint">
<SignInHint />
</div>
)}
</div>
)
}
function ReadRow({
label,
result,
error,
loading,
disabled,
onClick,
}: {
label: string
result: string | undefined
error: string | undefined
loading: boolean
disabled: boolean
onClick: () => void
}) {
return (
<div className="provider-reads-row">
<div className="provider-reads-row-text">
<span className="provider-reads-row-label">
{label}
</span>
<span
className={
error
? "provider-reads-row-value is-error"
: "provider-reads-row-value"
}
>
{error ?? result ?? "—"}
</span>
</div>
<Button
variant="secondary"
onClick={onClick}
disabled={disabled}
>
{loading ? "…" : "Read"}
</Button>
</div>
)
}
provider.reader({ chain_id }) and
create_reader(CHAINS)({ chain_id }) produce structurally
the same ResolvedReader. The method doesn't know which one
it's running against.