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.