⚠️ Alpha · testnets only. ChainRender is in active alpha and deployed on testnets only — Ethereum Sepolia, the Robinhood Chain Testnet, and the Circle Arc Testnet — not on any mainnet. Contracts, addresses, and APIs may change. Do not use with real funds or production assets.
Guides

Guide: Live Data, Events & Local State

Beyond binding a single value, @chainrender/client/runtime gives you batched reads, event log access, and opt-in local state — everything a live, reactive UI needs, while keeping the app itself fully on-chain.

Batched reads

Read several on-chain values in one round-trip with batchRead (backed by multicall):

import { batchRead } from "@chainrender/client/runtime";

const [price, balance, supply] = await batchRead([
    { to: oracle, sig: "latestAnswer()" },
    { to: token, sig: "balanceOf(address)", args: [user] },
    { to: token, sig: "totalSupply()" },
  ], { rpc });

Reading events

getLogs / readEvents read event history (for feeds, activity, indexing) with a topic helper:

import { eventTopic0, getLogs } from "@chainrender/client/runtime";

// getLogs(rpc) returns an EventSource; read a spec from it:
const source = getLogs(rpc);
const logs = await source.read({ to: token, event: "Transfer(address,address,uint256)" }, { fromBlock, toBlock });

For heavier discovery, the runtime exposes an indexer hook — but note the honest boundary: enumerating "all of X" at scale needs an indexer, while reading a specific value or a bounded range is a direct on-chain call. See The Render Pipeline.

Local state (opt-in, client-side)

For client-side preferences (a chosen theme, a saved RPC, UI state), the runtime offers pluggable local stores — memory, localStorage, indexedDB — so you don't reach for browser APIs directly and can keep behavior consistent and testable:

import { localStorage as crStore } from "@chainrender/client/runtime";
const store = crStore("myapp");
store.set("theme", "dark");

Local state never affects the app's on-chain identity or grade — it's purely a client convenience.

Binding data into a rendered component

bindSlots resolves an app's data slots against live sources at render time (this is what the interpreter does on-chain; the runtime mirror lets you drive it client-side for previews):

import { bindSlots } from "@chainrender/client/runtime";
const resolved = await bindSlots({ rpc, html, resolvers });

Live data inside a rich-media sketch (window.CR_CHAIN)

The reads above are the declarative path — data slots resolved on-chain by the interpreter. A rich-media p5/three sketch reads live data a different way: through the injected window.CR_CHAIN bridge. Same chain, same honesty rules, but callable imperatively from inside the sketch loop.

// snapshot (auto-polled, ready ~1-2s after start — not in setup()):
CR_CHAIN.blockNumber, CR_CHAIN.gasPriceGwei, CR_CHAIN.baseFeeGwei, CR_CHAIN.live

// async helpers (Promise-returning, fail-closed to null):
await CR_CHAIN.erc20(token)        // { name, symbol, decimals, totalSupply, totalSupplyFloat }
await CR_CHAIN.receipt(txHash)     // { status: "success"|"reverted", gasUsed, ... }
await CR_CHAIN.feeHistory(30)      // real gas curve: { baseFeeGwei:[...], gasUsedRatio:[...], reward:[[...]] }
await CR_CHAIN.recentLogs(addr, transferTopic, 2000)  // recent events, auto-ranged
await CR_CHAIN.ensName(addr)       // "vitalik.eth" or null

Use CR_CHAIN.onReady(fn) (or gate on CR_CHAIN.live in draw()) for the initial fetch, scale token amounts by the token’s real decimals (never a hardcoded 1e6/1e18), cache one-time reads, throttle repeats, and only show a "LIVE" label when CR_CHAIN.live === true. The full helper reference and honesty rules live in Rich Media → Reading live on-chain data.

Same honest boundary as above: erc20, receipt, feeHistory, logs, and ENS are all direct on-chain reads; enumerating "all holders" or "every historical transfer" at scale still needs an indexer, not a single call.

Related