BlockVectra

Token 元数据数据集(`tokens`)

{db}.tokens(DDL:sql/tokens.sql)缓存每个 ERC20/ERC721 token 地址的链上元数据:name / symbol / decimals / total_supply。这些字段不是从 logs 解码出来的(Transfer 事件里根本没有这些信息),而是由 chain…

This content is sourced from upstream and is currently available in Chinese only.

{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):

addressnamesymboldecimals
322f0929c4625ed5bad873c95208d54e1c003b2dTesla • Robinhood TokenTSLA18
af3d76f1834a1d425780943c99ea8a608f8a93f9Apple • Robinhood TokenAAPL18
d0601ce157db5bdc3162bbac2a2c8af5320d9eecNVIDIA • Robinhood TokenNVDA18
c0d6457c16cc70d6790dd43521c899c87ce02f35Meta Platforms • Robinhood TokenMETA18
2e0847e8910a9732eb3fb1bb4b70a580adad4fe3Alphabet Class A • Robinhood TokenGOOGL18

对应一笔真实转账(生产环境 robinhood.erc20_transfers,token = TSLA 地址):

tx_hash = 0xa8a7eca007dcd6a74e14c87a86f455a5949083ed3a0a7a71c1954718e3cdeff3
block_number = 72042425
amount (raw UInt256) = 4719130530095320

4. 查询时按 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.004719130530095320

Decimal256 精度上限 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' * d 100% 精确一致。而 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 进行字符串手工格式化,比对 Python Decimal 参考实现均完全一致(decimals=18 处于 pow 亦精确的区间,作为等价性基线验证);对 d ≥ 20 的测试(如 d=24),新方法消除了 pow 产生的除数与余数失真(如 12.345678901234567890123456 vs pow 的 12.345678901234568091314136)。

On this page