@chainrender/client
The off-chain hook layer for ChainRender. It does two things:
- Render an on-chain component to HTML through a free
eth_call(no wallet, no gas). - Bind the component's data-binding slots to live values (balances, ENS names, network, tx status).
It is framework-agnostic and dependency-free at its core (it speaks JSON-RPC directly and hand-rolls the small amount of ABI encode/decode it needs). A React adapter is provided as a separate entry point. This mirrors how Scaffold-ETH wraps wagmi — thin, typed helpers over the thing underneath — except the "thing underneath" is ChainRender's on-chain interpreter.
Quickstart
Install:
npm install @chainrender/client viem
Render an on-chain component to HTML in ~10 lines — no wallet, no gas, just a free eth_call:
import { renderComponent, bindSlots, staticResolvers, makeProviderResolvers } from "@chainrender/client";
import { createPublicClient, http } from "viem";
import { sepolia } from "viem/chains";
const deployment = {
rpc: "https://ethereum-sepolia-rpc.publicnode.com",
router: "0x0322b911900cb42694899eb06469c786f52e064c", // RenderRouter
registry: "0x36a443515b96b992ee397dfd9a533de72b70c3c9", // ComponentRegistry
};
// 1. render the component (by its content hash) to an escaped HTML string
const html = await renderComponent(deployment, "0x…componentHash");
// 2. mount it in an isolated shadow root
const shadow = document.getElementById("app").attachShadow({ mode: "open" });
shadow.innerHTML = html;
// 3. fill any live data-binding slots (balances, ENS, tx status)
const publicClient = createPublicClient({ chain: sepolia, transport: http(deployment.rpc) });
await bindSlots(shadow, { ...staticResolvers, ...makeProviderResolvers(publicClient) });
That's the whole loop: render on-chain → mount → bind live data. viem is a peer dependency (any compatible provider works); React and Farcaster Mini App adapters are optional entry points (see below). To discover component hashes, index the registry's ComponentRegistered event, or use enumerate(deployment) for a quick proof-of-concept list.
Why slots
ChainRender components are immutable, content-addressed, and stored on-chain, so live data (a balance that changes every block, an ENS name resolved via CCIP-Read) can't be baked into them. Instead the on-chain interpreter emits a slot — an escaped placeholder like:
<span class="cr-slot cr-slot-balance" data-cr-bind="balance" data-cr-arg="0x…">—</span>
The placeholder renders its fallback until this library fills it. ENS and balance bindings are inherently client-side because a contract can't perform the CCIP-Read HTTP that ENS resolution needs; that's the whole reason this off-chain layer exists alongside any on-chain resolver hook.
Core usage (any framework)
import { renderComponent, bindSlots, staticResolvers, makeProviderResolvers } from "@chainrender/client";
const deployment = { rpc: "https://…", router: "0x…", registry: "0x…" };
// 1. render on-chain -> escaped HTML string
const html = await renderComponent(deployment, componentHash);
// 2. mount into an isolated container (shadow root recommended)
const host = document.createElement("div");
const shadow = host.attachShadow({ mode: "open" });
shadow.innerHTML = html;
// 3. fill the slots with live data (pass a viem PublicClient, or any compatible provider)
await bindSlots(shadow, { ...staticResolvers, ...makeProviderResolvers(publicClient) });
enumerate(deployment) lists registered components (proof-of-concept on-chain enumeration; a
production app should index the registry's ComponentRegistered event instead).
React usage
import { ChainRenderView, useChainRenderComponent } from "@chainrender/client/react";
import { staticResolvers, makeProviderResolvers } from "@chainrender/client/resolvers";
const resolvers = { ...staticResolvers, ...makeProviderResolvers(publicClient) };
<ChainRenderView deployment={deployment} hash={hash} resolvers={resolvers} styleSheet={themeCss} />
styleSheet accepts the off-chain theme stylesheet you export from the ChainRender Explorer, so
the component picks up your colors and type scale. react is an optional peer dependency; the
core entry has no framework dependency.
Built-in slot bindings
staticResolvers cover the bindings that need no chain access (address, networkName,
avatar placeholder, txStatus fallback). makeProviderResolvers(provider) adds the live ones
(balance, ensName, ensNameOrAddress, avatar, networkName) using a viem-compatible
provider. Add your own by extending the resolver map — the key is the slot's data-cr-bind
kind.
First-party on-chain resolver kinds
Separate from the client-side runtime resolvers above (which fill slots in the browser), some slots
bind to an on-chain resolver that serves a kind. ChainRender ships a large first-party set — DeFi, oracle,
credentials, ENS, attestations, RWA, reputation, CCIP, messaging, settlement, cross-chain proofs and more —
listed in RESOLVER_KINDS and vocabulary().resolverKinds (kind, args, data edge, capability, notes) and exposed
per family under kinds, with their KIND_* constants and argument encoders, for example:
kinds.oracle— oracle feeds (KIND_FEED,encodeFeedArgs).kinds.credential/kinds.credentialBound— selective-disclosure and bound single-use credentials.kinds.ens—encodeAddrArgs(node),encodeTextArgs(node, key),encodeNameArgs(address),encodeV2Args(resolver, inner, isText)withv2AddrCall/v2TextCall.
Encode a slot's arg exactly as the kind's descriptor says: abi.encode of its args, or the raw bytes when the
descriptor has argsEncoding: "packed" (ensName takes exactly the 20 address bytes). The full list is at
https://chainrender.org/docs/reference/resolver-kinds.html. Resolver kinds are first-party but extensible — you can deploy your
own resolver serving a new kind; this is the set ChainRender ships, not a closed vocabulary.
Checking a tree before you compile it
compile() silently drops anything it doesn't read — an unknown field, an enum value that isn't in the dictionary
(role: "note"), a11y fields on an image, args on a batch member. checkNode(tree) reports each with its path
and enforces the node/depth/size limits; nodeJsonSchema() is the same rules as a strict JSON Schema for AI tooling.
import { checkNode, nodeJsonSchema } from "@chainrender/client";
const r = checkNode(tree); // { ok, issues: [{ path, code, message }], nodeCount, depth }
Safety
bindSlots sets textContent, so resolved values are inserted as text and never parsed as
HTML. The interpreter's output is already escaped. Render into a shadow root or a sandboxed
iframe under a strict Content-Security-Policy rather than a naive innerHTML on a privileged
page.
Install and networks
npm install @chainrender/client viem
viem is a peer dependency. The package is chain-agnostic — it hardcodes no addresses, no
chainId, and no RPC. You inject a client/RPC and the contract addresses from your deployment, so the
same package works on Sepolia, mainnet, or any L2:
import { createPublicClient, http } from "viem";
import { sepolia } from "viem/chains";
const client = createPublicClient({ chain: sepolia, transport: http(RPC_URL) });
// point renders/binds at the addresses from your deployment's addresses.json
Sepolia notes (data, not code): use the Sepolia deployment's addresses; if a component binds a Chainlink feed, use Sepolia feed addresses (or the deployed mock) — a mainnet feed address reverts on Sepolia; and ENS resolves mechanically but Sepolia has far fewer registered names.
Build
npm run build # tsc -p tsconfig.build.json → self-contained dist/
The build compiles the client together with the chainc/ mirrors into dist/, so the published
package has no imports that escape it. prepublishOnly runs it automatically before npm publish.
Distribution: Farcaster Mini App / standard web app
@chainrender/client/miniapp is a thin, fail-closed wrapper that presents a ChainRender app as a
Farcaster Mini App and a plain standard web app. The on-chain render is the floor and works
standalone; the Mini App layer (embed tags, FID / Sign In With Farcaster, notifications, wallet) is
progressive enhancement that no-ops outside a Farcaster host.
import { buildManifest, buildEmbedMetaTags, capabilitiesForEnvelope } from "@chainrender/client/miniapp";
import { mountChainRenderApp, signInWithFarcaster } from "@chainrender/client/miniapp";
// build /.well-known/farcaster.json (add the account association from the Farcaster dev tools)
const manifest = buildManifest({
name: "My Component", homeUrl: "https://my.app", iconUrl: "https://my.app/icon.png",
requiredChains: ["eip155:11155111"], // Sepolia
requiredCapabilities: capabilitiesForEnvelope(envelope), // carries the ChainRender capability honesty
});
// in the page: render the on-chain floor, then signal the host
await mountChainRenderApp(async () => { /* inject the eth_call HTML into the DOM */ });
@farcaster/miniapp-sdk is an optional peer dependency, loaded lazily — the adapter builds and
runs without it (standard web app mode). See examples/miniapp-wrapper.html for a complete page that
is both a web app and a Mini App.