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)
viewonly. 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.