Token 元数据数据集(`tokens`)
{db}.tokens(DDL:sql/tokens.sql)缓存每个 ERC20/ERC721 token 地址的链上元数据:name / symbol / decimals / total_supply。这些字段不是从 logs 解码出来的(Transfer 事件里根本没有这些信息),而是由 chain…
{db}.tokens(DDL:sql/tokens.sql)缓存每个 ERC20/ERC721 token
地址的链上元数据:name / symbol / decimals / total_supply。这些字段不是从
logs 解码出来的(Transfer 事件里根本没有这些信息),而是由
chain-indexer enrich-tokens 子命令(src/tokens.rs)对节点发起批量 eth_call
取回的,所以是一张需要单独跑一个命令来"回填"的表,不是物化视图。
1. 安装与补算(建表 + 运行)
# 建表({db} 换成实际链库名,例如 robinhood)
sed 's/{db}/robinhood/g' sql/tokens.sql | clickhouse-client --multiquery
# 首次全量回填,之后可以按 cron 跑增量(只会捞新出现或上次报错且过了重试窗口的地址)
CH_URL=... CH_USER=... CH_PASSWORD=... \
chain-indexer enrich-tokens --config chains/robinhood.toml --batch 50--limit N 限制单次处理的地址数,--batch 控制单次 eth_call 批量请求里打包的
token 数量(每个 erc20 地址 4 次调用,erc721 只有 name/symbol 2 次调用)。命令
按批次提交:一批写入 ClickHouse 后才处理下一批,中途被杀掉也不会丢已完成的部分,
重新跑会自动跳过已经成功的地址。
--candidates-from 选择候选地址来源(默认 derived):
derived(默认):读{db}.erc20_transfers/{db}.erc721_transfers。便宜,但 只覆盖这两张派生表已经建好的区块范围——在 2026-09-26 的robinhood上就是block_number >= 71,981,628的 follow 区间(窗口 2 重建完成前,更早的历史不在 这两张表里),只出现在更早区块的 token 对默认来源不可见。logs:直接扫{db}.logs里的Transfer(形状判据与 001/002 派生表逐字一致:topic1/topic2必须存在,topic3存在 = erc721、不存在且data恰 32 字节 = erc20)。覆盖全部已索引区块,代价是全表扫描(robinhood上 34 亿行 logs 实测约 2.5–5 分钟、单次 600 GB+ 解压后读放大)。首次全链回填、或派生表正在 重建时用这个来源。
--pause-ms 在每批 eth_call 之间插入休眠(默认 0),用于与同一节点上的 follow
共用带宽:每批是一个 HTTP 请求(--batch 50 的 erc20 = 200 次 eth_call),
所以这是唯一的限速旋钮。注意节点对单个 JSON-RPC 批次有上限(robinhood 的 Nitro
节点实测 ≤1000 次调用可用,1100 次起整批返回 -32600 batch too large,工具会直接
报错退出而不是写入坏数据),因此 --batch 不宜超过 250 个 erc20 地址。
元数据 eth_call 一律用 latest 块标签(不要改成固定块号):robinhood 的节点
不是 archive 节点,只保留很窄的一段历史状态(2026-09-26 实测:head-1000 可查、
head-5000 返回 historical state ... is not available)。固定块号在候选扫描耗时数分钟
(101ms 出块下约 2400 个区块)、或全链回填跑几小时之后必然过期,届时每个调用都会被
记成一条"元数据失败"。
2. 地址来源与"unknown"
候选地址来自 Transfer 事件(而不是订阅一个 token 白名单),所以覆盖的是"真实
发生过转账"的 token;两种来源(见 §1 的 --candidates-from)都是同一个形状判据,
区别只在扫的表的覆盖范围。unknown 这个 standard 枚举值目前不会被
enrich-tokens 自动产出,是留给手工插入场景的。
3. 失败与重试
name()/symbol()/decimals()/totalSupply() 里任何一次调用 revert、返回空
数据,或者返回的字节解不出预期类型,都不会让整条命令失败——那一次调用对应的字段
记成 NULL,error 列拼上一条 "字段: 原因",同一地址其他调用照常解析。下次
enrich-tokens 运行会在 6 小时的重试窗口过后重新尝试这些地址。
实测:本地拿 220 个从生产环境(robinhood,只读,max_threads=2 /
max_execution_time=90)按转账笔数采样的真实地址(160 个 ERC20 + 60 个
ERC721,涵盖多个 Robinhood 股票代币)灌进本地 ClickHouse 的一个 scratch db,
对真实节点跑 enrich-tokens:220/220 全部成功解析出 name/symbol
(ERC20 的 decimals/total_supply 也全部成功)。额外插入一个已知无合约代码的
地址(0x000000000000000000000000000000000000dead)验证失败路径:4 次调用全部
拿到空返回,全部记成 NULL,error 为
name: empty return data; symbol: empty return data; decimals: empty return data; totalSupply: empty return data;
再跑一次 enrich-tokens,候选数正确变成 0(重试窗口内不会重复处理),验证了幂等性。
样例数据(hex(address) / name / symbol / decimals):
| address | name | symbol | decimals |
|---|---|---|---|
322f0929c4625ed5bad873c95208d54e1c003b2d | Tesla • Robinhood Token | TSLA | 18 |
af3d76f1834a1d425780943c99ea8a608f8a93f9 | Apple • Robinhood Token | AAPL | 18 |
d0601ce157db5bdc3162bbac2a2c8af5320d9eec | NVIDIA • Robinhood Token | NVDA | 18 |
c0d6457c16cc70d6790dd43521c899c87ce02f35 | Meta Platforms • Robinhood Token | META | 18 |
2e0847e8910a9732eb3fb1bb4b70a580adad4fe3 | Alphabet Class A • Robinhood Token | GOOGL | 18 |
对应一笔真实转账(生产环境 robinhood.erc20_transfers,token = TSLA 地址):
tx_hash = 0xa8a7eca007dcd6a74e14c87a86f455a5949083ed3a0a7a71c1954718e3cdeff3
block_number = 72042425
amount (raw UInt256) = 47191305300953204. 查询时按 decimals 还原金额(禁止用 float)
erc20_transfers.amount 是原始 UInt256,tokens.decimals 才知道要除以
10^decimals。和 queries.md §1.3 一样:
UInt256 一律 toString() 读出来,金额换算也不能用浮点,两种写法二选一。
4.1 toDecimal256(推荐,值不太极端时最简单)
SELECT
hex(t.tx_hash) AS tx_hash_hex,
tk.symbol,
-- decimals 是变长的,toDecimal256 的 scale 参数必须是常量,
-- 所以这里用 multiIf 覆盖数据集里实际出现过的 decimals 取值;
-- 新增一档 decimals 就在这加一条分支。
multiIf(
tk.decimals = 18, toDecimal256(t.amount, 18),
tk.decimals = 9, toDecimal256(t.amount, 9),
tk.decimals = 8, toDecimal256(t.amount, 8),
tk.decimals = 6, toDecimal256(t.amount, 6),
toDecimal256(t.amount, 0) -- 未覆盖的 decimals:先按整数处理,人工核对
) AS amount_decimal
FROM robinhood.erc20_transfers AS t FINAL
INNER JOIN robinhood.tokens AS tk FINAL ON tk.address = t.token
WHERE t.token = unhex('322f0929c4625ed5bad873c95208d54e1c003b2d')
AND t.is_deleted = 0
ORDER BY t.block_number DESC
LIMIT 3;
-- amount_decimal 示例:4719130530095320 / 10^18 = 0.004719130530095320Decimal256 精度上限 76 位有效数字,decimals 越大能表示的整数部分位数越少
(76 - decimals);total_supply 这类可能非常大的值换算前先用
decimals <= 18 这类已知上限过滤,或者改用下面的字符串方案。
4.2 字符串手工格式化(total_supply 等可能超出 Decimal256 表示范围时)
SELECT
hex(tk.address) AS address_hex,
tk.symbol,
tk.decimals,
toString(tk.total_supply) AS total_supply_raw,
-- 整数部分 = 除以 10^decimals 取商;小数部分 = 余数左侧补零到 decimals 位。
-- 10^decimals 使用 toUInt256(concat('1', repeat('0', toUInt32(tk.decimals)))) 做精确构造,
-- 禁止使用 pow(10, tk.decimals)(pow 返回 Float64,仅在 d ≤ 19 时精确,从 d ≥ 20 开始失真,如 d=20 时为 100000000000000000003,d=24 时误差达 16,765,890)。
concat(
toString(intDiv(tk.total_supply, toUInt256(concat('1', repeat('0', toUInt32(tk.decimals)))))),
'.',
leftPad(toString(tk.total_supply % toUInt256(concat('1', repeat('0', toUInt32(tk.decimals))))), tk.decimals, '0')
) AS total_supply_human
FROM robinhood.tokens AS tk FINAL
WHERE tk.symbol = 'TSLA';两种写法都只在 SQL 侧做整数运算/字符串拼接,amount/total_supply 以及
10^decimals 比例因子本身从未被转换成 float——这与仓库"内部核心逻辑禁止浮点"的原则一致(这里是查询层,
不是 Rust 核心逻辑,但同一条金钱安全的红线适用)。
[!NOTE] 精确缩放与浮点误差对比验证: 在 ClickHouse 中对 decimals =
[0, 6, 8, 18, 24, 30, 77]验证toUInt256(concat('1', repeat('0', toUInt32(d)))),其字符串表示与标准的'1' + '0' * d100% 精确一致。而pow(10, d)::UInt256仅在 d ≤ 19 时精确,从 d ≥ 20 起产生失真:d=20 时得到100000000000000000003(误差 +3);d=24 时得到999999999999999983234110(误差 16,765,890);d=30 和 d=77 时精度彻底失效。针对 TSLA、AAPL、NVDA 真实代币的total_supply进行字符串手工格式化,比对 PythonDecimal参考实现均完全一致(decimals=18 处于 pow 亦精确的区间,作为等价性基线验证);对 d ≥ 20 的测试(如 d=24),新方法消除了pow产生的除数与余数失真(如12.345678901234567890123456vs pow 的12.345678901234568091314136)。
ArbOS L1 计价历史数据集(arbos_l1_pricing / daily_l1_base_fee_stats)
来源:schema/clickhouse/derived/022_arbos_l1_pricing.sql。 面向内部用 SQL 查询 Arbitrum Orbit L1 计价状态、L1 区块对齐与跨链批量发布历史的团队。
Token 供应量数据集(Token Supply)
来源:schema/clickhouse/derived/019_token_supply.sql。 面向内部用 SQL 追踪和分析各 ERC-20 代币每日发行量(Mint)、销毁量(Burn)、每日净增量(Net Mint)以及历史累计流通供应量(Cumulative Supply)的团队。