Show a revenue token
Tell a revenue token apart, then show its balance, income, drops, streams and when they end, its page and its mark.
On this page
A wallet or an explorer that knows a token is a revenue token can show more than its balance: the holder's income in each asset, its drops, and each stream with when it ends. This walkthrough builds that view for any token and wallet.
The steps
- Ask the tokenizer:
tokenizations(token)has a nonzerowayonly for a token it made. Show anything else as a plain ERC-20. - Read the holder's side:
balanceOf,earns,claimable(account, i)for each asset, and the drop box'sclaimable(dropId, account)for each drop. - Read the token's side:
streamSeconds()for the release period, andstreamsOf(token)on the tokenizer for its streams. - Turn each stream's end into words.
AT_BLOCKcounts the parent chain's blocks, about 12 seconds each, so a date comes from the contracts' clock, never frometh_blockNumber.REVOCABLEmeans someone else can move the stream away: for a Pons door, Pons, after three days' notice. With a time, a move is pending from then. - Link the token's page and draw its mark, from your own copy of it.
In code
With client.ts and abis.ts:
import { erc20Abi, formatUnits, zeroAddress, type Address } from "viem";
import { client, parentBlock, TOKENIZER } from "./client";
import { dropBoxAbi, tokenAbi, tokenizerAbi } from "./abis";
const STREAM = ["Rules ledger", "launched family", "its own market", "pools.trade fee NFT", "Uniswap v4 positions", "Uniswap v3 positions", "outside door"];
const day = (seconds: bigint) => new Date(Number(seconds) * 1000).toDateString();
export async function showRevenueToken(token: Address, account: Address) {
// Only the tokenizer's record says a token is one of its revenue tokens.
const record = await client.readContract({ address: TOKENIZER, abi: tokenizerAbi, functionName: "tokenizations", args: [token] });
if (record.way === 0) return null;
const t = { address: token, abi: tokenAbi } as const;
const [symbol, balance, earns, period, assetCount, streams, box, clock] = await Promise.all([
client.readContract({ ...t, functionName: "symbol" }),
client.readContract({ ...t, functionName: "balanceOf", args: [account] }),
client.readContract({ ...t, functionName: "earns", args: [account] }),
client.readContract({ ...t, functionName: "streamSeconds" }),
client.readContract({ ...t, functionName: "assetCount" }),
client.readContract({ address: TOKENIZER, abi: tokenizerAbi, functionName: "streamsOf", args: [token] }),
client.readContract({ ...t, functionName: "dropBox" }),
parentBlock(),
]);
// Income to claim, per asset, each in its own decimals.
const income: string[] = [];
for (let i = 0n; i < assetCount; i++) {
const { currency } = await client.readContract({ ...t, functionName: "assetAt", args: [i] });
const [name, decimals] =
currency === zeroAddress
? (["ETH", 18] as const)
: await Promise.all([
client.readContract({ address: currency, abi: erc20Abi, functionName: "symbol" }),
client.readContract({ address: currency, abi: erc20Abi, functionName: "decimals" }),
]);
const due = await client.readContract({ ...t, functionName: "claimable", args: [account, i] });
income.push(`${formatUnits(due, decimals)} ${name}`);
}
// Drops to claim from the drop box.
const dropCount = await client.readContract({ address: box, abi: dropBoxAbi, functionName: "dropCount" });
const drops: bigint[] = [];
for (let id = 0n; id < dropCount; id++) {
if ((await client.readContract({ address: box, abi: dropBoxAbi, functionName: "claimable", args: [id, account] })) > 0n) drops.push(id);
}
// Each stream and how it ends. A parent-chain block is about 12 seconds.
const now = BigInt(Math.floor(Date.now() / 1000));
const ends = (endKind: number, end: bigint) =>
endKind === 0 ? "forever"
: endKind === 1 ? `around ${day(now + (end - clock) * 12n)} (parent block ${end})`
: endKind === 2 ? `on ${day(end)}`
: endKind === 3 ? (end === 0n ? "can be moved away" : `moves away from ${day(end)}`)
: "ended";
return {
symbol,
balance: formatUnits(balance, 18),
earning: earns,
releasePeriodDays: period / 86_400,
income,
drops,
streams: streams.map((s) => `${STREAM[s.kind]}: ${ends(s.endKind, s.end)}`),
page: `https://hookr.fun/revenue/${token}`,
mark: `https://hookr.fun/api/revenue-tokens/${token}/mark`, // fetch it on your server and serve a copy
};
}For rHOOKR and the leg collector, it gives a balance of 50,000,000 that earns, a release period of 1 day, the collector's ETH income to claim, and three streams: the pools.trade fee NFT, the Rules ledger and its own market, each forever. For a plain ERC-20 it gives null.
What to keep in mind
- A Long door reads
REVOCABLE, with no time, while its pool could still graduate, which would end the stream, or when the pool's other currency is an ERC-20 whose issuer could block it. - An outside door other than the Pons and Long doors is unverified: show what it calls itself,
label(), as its own claim. claimablegrows every second while income is released, so refresh it rather than add to it.- The mark at
https://hookr.fun/api/revenue-tokens/<address>/markis served for hookr.fun's own pages. Fetch it on your server and serve your copy.