⚠️ 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: Writing a Custom Resolver

Resolvers are how a ChainRender app gets any on-chain data or media. ChainRender ships a large catalog, but you can write your own — it's a tiny view contract.

The interface

A resolver implements two functions:

interface IChainRenderResolver {
    function supportsKind(bytes32 kind) external view returns (bool);
    function resolve(bytes32 kind, bytes calldata args) external view returns (string memory);
}
  • supportsKind — a capability check (does this resolver serve this data kind?).
  • resolve — read chain state and return a string (the value to display). The interpreter escapes it, so you can't inject markup even if you tried.

Example: a token-balance resolver

contract BalanceResolver is IChainRenderResolver {
    bytes32 public constant KIND = "tokenBalance";

    function supportsKind(bytes32 kind) external pure returns (bool) {
        return kind == KIND;
    }

    function resolve(bytes32 kind, bytes calldata args) external view returns (string memory) {
        if (kind != KIND) return "unavailable";           // fail closed on unknown kind
        (address token, address who) = abi.decode(args, (address, address));
        try IERC20(token).balanceOf(who) returns (uint256 bal) {
            return _format(bal);                            // your formatting
        } catch {
            return "unavailable";                           // fail closed on revert
        }
    }
}

The rules that keep it safe (and Grade A)

  • view only. A resolver reads; it never writes or moves value. This is why binding data keeps an app Grade A.
  • Fail closed. Return an honest fallback ("unavailable") on any error — never revert the whole render, never fabricate a value.
  • No markup. Return plain text; the interpreter escapes it. Never assume your output is trusted HTML.
  • Structured variants. For charts/tables/routes, implement the richer interfaces (IChartData, ITableSource, IRouteSource) instead — same discipline, structured return.

Binding it in your app

import { compose } from "@chainrender/client";
// a data slot pointing at your resolver + kind:
compose.slot("tokenBalance", { resolver: "0xYourResolver", arg: encodedArgs  }) // slot(bind, { resolver, arg });

Testing your resolver's gas

A resolver bound to a slot runs under a gas stipend. Before binding a gas-heavy resolver to a value slot, probe it: if it exceeds the stipend it should fail closed to the fallback (and you'd bind it as an image slot, which gets a larger budget, instead). Test your resolver returns a sensible fallback under a tight gas limit.

Related