⚠️ 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.
Concepts

Rendering Uniswap V4 Hooks (and any contract's view state)

You do not need a special "hook resolver." A Uniswap V4 hook is just a contract, and ChainRender already ships a generic, robust, ethos-compliant way to read + render any contract's view function: the CallResolver (kind = "staticread") and, for several fields at once, MultiCallResolver (kind = "multiread"). This page shows how to use them to render live hook state.

Why not a dedicated hook resolver? Because it would duplicate CallResolver. ChainRender's design is modular + additive: one generic view-reader serves hooks, oracles, ERC-20s, governance, and anything else with a view function. Fewer contracts, one audited path, no per-integration special cases.

What CallResolver does (kind "staticread")

args = abi.encode(address target, bytes callData, uint8 retType, uint8 fmt, uint8 dec, uint8 frac)

  • target — the contract to read (the hook address, or a StateView, or any contract).
  • callData — the encoded view call (selector + args), e.g. abi.encodeWithSelector(hook.swapCount.selector, poolId).
  • retType — how to interpret the returned word: 0=uint 1=int 2=address 3=bool 4=string 5=bytes32 (ReadFormat).
  • fmt / dec / frac — formatting: abbreviate (K/M/B/T), fixed-point decimals, fractional digits.

It staticcalls with a gas cap + reserve, then formats via ReadFormat. Fails closed to a fallback string on any revert, missing contract, or malformed return — never reverts the render. Immutable, ownerless, view-only.

Example: rendering a hook's swap counter (DinoHook-style)

Say a V4 hook exposes function swapCount(bytes32 poolId) external view returns (uint256). To render it:

bytes memory callData = abi.encodeWithSelector(0xXXXXXXXX /* swapCount */, poolId);
bytes memory args = abi.encode(
    hookAddress,               // target = the hook
    callData,                  // the view call
    uint8(0),                  // retType = RET_UINT
    uint8(1),                  // fmt = abbreviate (12.3K)
    uint8(0), uint8(0)         // dec, frac
);
// resolver kind "staticread"; the component slot references this and renders the number live.

In the visual builder, this is a data-bound slot: point it at CallResolver with kind staticread and those args. The rendered component shows the hook's live swap count, read on-chain during the eth_call render \u2014 no server, no indexer, fail-closed if the hook is unreachable.

Reading several hook fields at once (kind "multiread")

For a hook dashboard \u2014 swap count, current dynamic fee, last swapper, TVL \u2014 use MultiCallResolver ("multiread"), which batches multiple staticread-shaped calls into one rendered result. Same target/callData/ retType shape per field; one component, many live hook values.

What this renders (and what it doesn't)

  • Renders: any hook view/pure function \u2014 counters, dynamic fees (getSlot0().lpFee reflects hook-set fees), order-book sizes, reputation scores, custom curve params, whatever the hook exposes.
  • Does NOT execute: rendering is a view. To act on a hook (trigger a swap that runs the hook, place an order), that's the declared-action layer \u2014 a button whose action is decoded honestly before signing (see the action decoder + swap honesty flag). ChainRender renders hook state and declares hook-triggering actions; it never becomes the AMM.

The three ways ChainRender meets hooks (complete picture)

  1. Render hook-influenced pool state \u2014 UniswapV4PriceResolver reads getSlot0(poolId); the lpFee it returns is the current (possibly hook-adjusted) fee. (V3 pools: UniswapV3PriceResolver, kept alongside.)
  2. Render a hook's own state \u2014 CallResolver / MultiCallResolver read the hook's view functions (this page).
  3. A hook that drives ChainRender \u2014 e.g. DinoHook: swap activity in the hook feeds a ChainRender NFT's render. That's hooks feeding ChainRender, the reverse direction.

Expandability (the ethos-correct kind)

New return types or formats are added by versioning ReadFormat / deploying a new resolver additively \u2014 never by mutating a deployed contract. CallResolver is immutable and ownerless; if you ever need a capability it lacks, you deploy CallResolverV2 and reference it, leaving V1 untouched. That is how ChainRender "expands": more immutable pieces, never a mutable one.

Related

  • contracts/CallResolver.sol (staticread) \u2014 the generic view reader (renders any hook view function).
  • contracts/MultiCallResolver.sol (multiread) \u2014 batch several hook fields.
  • contracts/ReadFormat.sol \u2014 the return-type + formatting layer (uint/int/address/bool/bytes32/string).
  • contracts/UniswapV4PriceResolver.sol \u2014 render V4 pool price (reflects hook-set fees).
  • contracts/UniswapV3PriceResolver.sol \u2014 render V3 pool price (kept alongside V4).