Ethernauta

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 EIP-6963 example).
View on GitHub

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.