Pool metadata in one call
This is the flagship evs script: given a Uniswap V3 pool address, fetch the pool’s
configuration (token0, token1, fee, current tick) and each token’s symbol and
decimals — eight dependent reads, one eth_call, one fully typed result object.
The script
Section titled “The script”import { evscript, t } from '@maxencerb/evs';import { erc20Abi } from 'viem';
const uniswapV3PoolAbi = [ { type: 'function', name: 'token0', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'address' }], }, { type: 'function', name: 'token1', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'address' }], }, { type: 'function', name: 'fee', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'uint24' }], }, { type: 'function', name: 'slot0', stateMutability: 'view', inputs: [], outputs: [ { name: 'sqrtPriceX96', type: 'uint160' }, { name: 'tick', type: 'int24' }, { name: 'observationIndex', type: 'uint16' }, { name: 'observationCardinality', type: 'uint16' }, { name: 'observationCardinalityNext', type: 'uint16' }, { name: 'feeProtocol', type: 'uint8' }, { name: 'unlocked', type: 'bool' }, ], },] as const;
export const poolMeta = evscript( { name: 'poolMeta', args: [t.address] }, (s, pool) => { const token0 = s.read({ address: pool, abi: uniswapV3PoolAbi, functionName: 'token0' }); // ^? Expr<'address'> — a runtime value, usable as the address of the NEXT call const token1 = s.read({ address: pool, abi: uniswapV3PoolAbi, functionName: 'token1' }); const fee = s.read({ address: pool, abi: uniswapV3PoolAbi, functionName: 'fee' }); const slot0 = s.read({ address: pool, abi: uniswapV3PoolAbi, functionName: 'slot0' }); // ^? readonly tuple of Exprs — one per slot0 output
const symbol0 = s.read({ address: token0, abi: erc20Abi, functionName: 'symbol' }); const symbol1 = s.read({ address: token1, abi: erc20Abi, functionName: 'symbol' }); const decimals0 = s.read({ address: token0, abi: erc20Abi, functionName: 'decimals' }); const decimals1 = s.read({ address: token1, abi: erc20Abi, functionName: 'decimals' });
return s.return({ token0, token1, fee, tick: slot0[1], symbol0, symbol1, decimals0, decimals1, }); },);The single pool argument arrives as a positional callback param after s. s.read types
like viem’s readContract: the inline as const pool ABI and viem’s erc20Abi drive
autocomplete for functionName and the output types. token0 comes back as Expr<'address'> —
a handle to a value that exists only at run time — and goes straight into the address field of
the symbol and decimals calls. This slot0 returns seven flat outputs, so it is a readonly
tuple of Exprs and slot0[1] is the tick. See calls for the full s.read
surface and values and types for how Expr handles work.
When the getter returns a struct
Section titled “When the getter returns a struct”Some getters return a single Solidity struct instead of flat outputs — for example a
slot0Struct() whose one ABI output is a 'tuple'. evs decodes that into a
Tuple handle whose members you read by name:
import { evscript, t } from '@maxencerb/evs';
const slot0StructAbi = [ { type: 'function', name: 'slot0Struct', stateMutability: 'view', inputs: [], outputs: [ { name: '', type: 'tuple', components: [ { name: 'sqrtPriceX96', type: 'uint160' }, { name: 'tick', type: 'int24' }, { name: 'unlocked', type: 'bool' }, ], }, ], },] as const;
const poolSlot0 = evscript({ name: 'poolSlot0', args: [t.address] }, (s, pool) => { const slot0 = s.read({ address: pool, abi: slot0StructAbi, functionName: 'slot0Struct' }); // ^? Tuple<…> — the struct output, decoded return s.return({ sqrtPriceX96: slot0.sqrtPriceX96.get(), // Expr<'uint160'> tick: slot0.tick.get(), // Expr<'int24'> — read by name unlocked: slot0.unlocked.get(), // Expr<'bool'> slot0, // a Tuple handle is returnable directly — the whole struct flows out, abitype-typed });});slot0.tick.get() is fully typed Expr<'int24'>, and returning the slot0 handle directly hands
the whole struct back as one abitype-typed object (use slot0.expr() if you want the bare memref
Expr). See composite types.
The dependency chain multicall cannot express
Section titled “The dependency chain multicall cannot express”Trace the data flow: symbol0 is a call to token0, and token0 is the output of a
call to the pool. Multicall3 encodes its whole batch client-side — every target address and
every calldata blob must be known before the request is sent — so it can only batch
independent reads. Here the targets of calls five through eight are unknown until calls one
and two return, which forces a multicall user into two sequential round trips: one batch for
the pool reads, then a second batch for the token reads built from the first batch’s results.
evs records the dependency as a script and executes the whole chain inside a single
eth_call, so the result arrives in one round trip regardless of how deep the chain goes.
See why evs for the general argument.
Execution
Section titled “Execution”Compile the script and spread toViem() into a plain viem readContract. The default mode is
deployless: viem sends a two-parameter eth_call with the script’s init bytecode as code,
so it works on every standard RPC provider and nothing is ever deployed.
import { evscript, t } from '@maxencerb/evs';import { createPublicClient, erc20Abi, http } from 'viem';
// --- the script from the previous section, unchanged ---
const uniswapV3PoolAbi = [ { type: 'function', name: 'token0', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'address' }], }, { type: 'function', name: 'token1', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'address' }], }, { type: 'function', name: 'fee', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'uint24' }], }, { type: 'function', name: 'slot0', stateMutability: 'view', inputs: [], outputs: [ { name: 'sqrtPriceX96', type: 'uint160' }, { name: 'tick', type: 'int24' }, { name: 'observationIndex', type: 'uint16' }, { name: 'observationCardinality', type: 'uint16' }, { name: 'observationCardinalityNext', type: 'uint16' }, { name: 'feeProtocol', type: 'uint8' }, { name: 'unlocked', type: 'bool' }, ], },] as const;
const poolMeta = evscript({ name: 'poolMeta', args: [t.address] }, (s, pool) => { const token0 = s.read({ address: pool, abi: uniswapV3PoolAbi, functionName: 'token0' }); const token1 = s.read({ address: pool, abi: uniswapV3PoolAbi, functionName: 'token1' }); const fee = s.read({ address: pool, abi: uniswapV3PoolAbi, functionName: 'fee' }); const slot0 = s.read({ address: pool, abi: uniswapV3PoolAbi, functionName: 'slot0' }); const symbol0 = s.read({ address: token0, abi: erc20Abi, functionName: 'symbol' }); const symbol1 = s.read({ address: token1, abi: erc20Abi, functionName: 'symbol' }); const decimals0 = s.read({ address: token0, abi: erc20Abi, functionName: 'decimals' }); const decimals1 = s.read({ address: token1, abi: erc20Abi, functionName: 'decimals' }); return s.return({ token0, token1, fee, tick: slot0[1], symbol0, symbol1, decimals0, decimals1 });});
// --- compile once, call like any contract ---
const client = createPublicClient({ transport: http('https://ethereum-rpc.publicnode.com') });
const compiled = poolMeta.compile();
export const out = await client.readContract({ ...compiled.toViem(), // { abi, code } — deployless eth_call functionName: 'poolMeta', args: ['0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640'], // USDC/WETH 0.05% pool (mainnet)});// out.symbol0 → 'USDC' (string), out.decimals1 → 18 (number), out.token0 → 0x… (address)No as const, no codegen step: the script value is its own literal-typed ABI, so args is
typed readonly [`0x${string}`] and out is fully inferred. For the state-override mode,
block pinning, and the s.env caveats, see execution.
The result
Section titled “The result”The script’s return keys become the named components of a single tuple output, which viem decodes as an object:
{ token0: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', token1: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', fee: 500, tick: 197180, symbol0: 'USDC', symbol1: 'WETH', decimals0: 6, decimals1: 18}(Values illustrative — tick moves with the market.) The TypeScript type matches:
addresses as 0x${string}, fee/tick/decimals* as number (they fit in 48 bits),
symbols as string.
Run it from the repo
Section titled “Run it from the repo”A runnable version of this example — examples/pool-meta — spawns a throwaway local anvil, deploys mock pool and token contracts, executes the compiled script deploylessly, and prints the typed result plus the script’s runtime bytecode size. For batching the same read over many inputs, continue with token balances.