NASDANK Docs
Open app
Build/Reading token state

Reading token state

viem recipes for the things people actually need to read — a coin's price, its pool, its lock, and what a creator is owed. Every one reads from the chain, because the chain is the only thing that is authoritative about money.

Set up a client

client.ts
import { createPublicClient, http, defineChain } from  2 ;

export const robinhood = defineChain({
  id: 4663,
  name:  3 ,
  nativeCurrency: { name:  4 , symbol:  5 , decimals: 18 },
  rpcUrls: { default: { http: [ 6 Blockscout 7 https: 1 
});

export const client = createPublicClient({ chain: robinhood, transport: http() });

The price of a V3 pool

Never derive a price from reserves

The token balances a concentrated-liquidity pool holds are not a price. Two pools at identical prices can hold completely different reserves depending on where their liquidity sits. Read slot0.

Spot price from slot0
import { parseAbi, formatUnits } from  5 ;
import { client } from  6 ;

const POOL_ABI = parseAbi([
   7 ,
   8 ,
   9 ,
   10 ,
]);

const Q96 = 2n ** 96n;

export async function poolPrice(pool:  11 , tokenIsToken0: boolean) {
  const [slot0] = await client.readContract({ address: pool, abi: POOL_ABI, functionName:  12  });

   0 
  const ratioX192 = slot0 * slot0;
  const num = ratioX192;
  const den = Q96 * Q96;

   1 
   2 
  const SCALE = 10n ** 36n;
  const token1PerToken0 = (num * SCALE) / den;

   3 
   4 
  const priceScaled = tokenIsToken0 ? token1PerToken0 : (SCALE * SCALE) / token1PerToken0;
  return formatUnits(priceScaled, 36);
}
tokenIsToken0 is not optional

Token ordering in a pool falls out of address sort order, so whether the pool's price is your coin's price or its reciprocal is effectively random per coin. The indexer publishes tokenIsToken0 on every token record; read it, or derive it by comparing your token address against token0(). Assuming either way produces a price that is wrong by a factor of the price squared on half of all coins.

The price of a V4 pool

V4 has no per-pool contract. Read through StateView, keyed by pool id.

V4 pool state via StateView
const STATE_VIEW =  1 ;  0 

const SV_ABI = parseAbi([
   2 ,
   3 ,
]);

const [sqrtPriceX96, tick] = await client.readContract({
  address: STATE_VIEW,
  abi: SV_ABI,
  functionName:  4 ,
  args: [poolId],
});
StateView is per-chain

Robinhood's StateView address has zero bytes of code on BSC. Hardcoding one and using it on the other makes every V4 price read throw, which surfaces to a user as "could not read the pool price" on a pool that is perfectly healthy.

Uncollected fees on a Pro position

This is the number a creator dashboard actually needs, and the one a splitter-balance read misses entirely. Uniswap exposes it by simulating a collect with the maximum amounts — the call is view-safe when simulated.

What a Pro position has earned but not yet paid out
const NPM =  2 ;

const NPM_ABI = parseAbi([
   3 ,
   4 ,
]);

const MAX_U128 = (1n << 128n) - 1n;

 0 
const owner = await client.readContract({
  address: NPM, abi: NPM_ABI, functionName:  5 , args: [tokenId],
});

const { result } = await client.simulateContract({
  address: NPM,
  abi: NPM_ABI,
  functionName:  6 ,
  args: [{ tokenId, recipient: owner, amount0Max: MAX_U128, amount1Max: MAX_U128 }],
  account: owner,
});
 1 

Who holds the LP

Confirm the position is in a locker
const positionOwner = await client.readContract({
  address: NPM, abi: NPM_ABI, functionName:  1 , args: [tokenId],
});

 0 
const LAUNCHER_ABI = parseAbi([ 2 ]);
const locker = await client.readContract({
  address: launcherThatEmittedTheEvent,
  abi: LAUNCHER_ABI,
  functionName:  3 ,
});

const locked = positionOwner.toLowerCase() === locker.toLowerCase();

Token basics

Supply, decimals, balances
const ERC20 = parseAbi([
   0 ,
   1 ,
   2 ,
   3 ,
   4 ,
]);

const [name, symbol, decimals, supply] = await Promise.all([
  client.readContract({ address: token, abi: ERC20, functionName:  5  }),
  client.readContract({ address: token, abi: ERC20, functionName:  6  }),
  client.readContract({ address: token, abi: ERC20, functionName:  7  }),
  client.readContract({ address: token, abi: ERC20, functionName:  8  }),
]);

Batch reads through Multicall3 at 0xcA11bde0…76CA11 rather than sending them one at a time. A page that prices ninety tokens sequentially takes tens of seconds; the same reads batched take under one.

Quoting a trade

Quote from the chain, never from an indexer, and never from a price you cached. QuoterV2 for V3, the V4 Quoter for V4. Then simulate the actual swap with eth_call before signing — if the simulation reverts, do not send it.

The order that matters

Quote → compute a fresh minimum received → simulate the exact transaction → sign. Skipping the simulation is what turns a recoverable "this would have failed" into a burnt gas fee and a confused user.

Holder sets

There is no holders() function on an ERC-20. A holder set has to be reconstructed from Transfer logs, which means it inherits the RPC's log limits in full — see Onchain events for the split-and-recurse pattern that survives them.

The burn address is not a holder

0x…dEaD and the zero address will appear in a naive holder set built from transfers, and both will rank near the top on any coin with a burn. Exclude them explicitly — including them in a reward denominator dilutes every real holder.

© NASDANK Not affiliated with Robinhood Markets, Inc. Risk disclosures nasdank.fun