钱包代币余额 API:ERC-20 资产与转账历史

构建钱包资产页:读取地址的非零 ERC-20 余额与转账历史,转账窗口必填 from_block 与 to_block,可批量补齐代币元数据,一次最多 100 个地址,余额按精度换算后再展示,分页读取全部转账。

用钱包数据 API 构建资产页:通过代币余额 API读取非零 ERC-20 持仓,通过代币转账 API读取钱包历史。开发者与 AI Agent 使用同一套认证请求。查询前读取 GET /v1/status,检查所选链的 data_features 与 data_status;余额覆盖因链而异。请求参数与响应格式见 Data API 参考。

这篇帮你完成什么任务

钱包资产页需要哪三类数据

钱包资产页可以展示地址的 ERC-20 代币余额、代币转账历史与代币元数据。Data API 为这三类数据分别提供接口:

  • 余额:GET /{chain}/addresses/{address}/balances,返回该地址的非零 ERC-20 余额,按 token 地址升序排列;在可获取时附带代币的 symbol 与 decimals。没有余额的地址返回 200,data 为空数组。
  • 转账:GET /{chain}/addresses/{address}/transfers,返回该地址在必填区块窗口内的代币转账,按 (block_number, log_index) 降序排列。
  • 代币元数据:GET /{chain}/tokens/{token} 按合约地址读取单个代币的名称、符号、精度与总供应量;POST /{chain}/tokens:batch 一次最多为 100 个地址批量读取同样的元数据。

三个接口都以 https://api.blockvectra.com/v1/data 为基地址,用 x-api-key 请求头鉴权,链名示例用 robinhood_mainnet。它们分别属于 balances、transfers 与 token_metadata 三个能力;某条链是否提供某个能力,以支持的链页面为准。链上没有该能力时,接口返回 422 no_coverage。

请求一:地址余额

这个接口要求的参数较少,适合作为页面首屏的第一个请求:

  • {chain}(路径参数,必填):链标识,即 GET /chains 中某个条目的 chain 值(例如 robinhood_mainnet);匹配精确且大小写敏感,别名与数字链 ID 不被接受。
  • {address}(路径参数,必填):20 字节地址,0x 前缀可选、大小写均可。
  • limit(查询参数,可选):每页条数。缺省为 50;大于 500 会收敛为 500;传 0 或非整数返回 400 bad_request。
  • cursor(查询参数,可选):上一页响应中 next_cursor 的值,原样传回即可取下一页。游标只对签发它的链、端点和查询参数有效,用在其他链或参数上会返回 400 bad_request。

它使用游标分页:next_cursor 只在确实还有下一页时才出现,最后一页该字段整个不存在,绝不会是 null。

export BLOCKVECTRA_API_KEY=rgw_your_api_key

curl -s "https://api.blockvectra.com/v1/data/robinhood_mainnet/addresses/0x1111111111111111111111111111111111111111/balances?limit=50" \
  -H "x-api-key: $BLOCKVECTRA_API_KEY"

响应信封为 AddressBalanceListEnvelope,包含 data 与 meta。data 的每一项是一个 AddressBalance:

字段类型说明
tokenstring(地址)代币合约地址,规范形式为 0x 加 40 位小写十六进制字符。
balancestring(十进制)原始整数余额,可能超过 2^53,以纯十进制字符串返回,绝不使用 JSON number、科学计数法或十六进制。
symbolstring 或 null代币符号;不可用时为 null。
decimalsinteger 或 null代币精度,取值 0–255;不可用时为 null。

请求二:地址转账

转账接口要求一个显式的区块窗口:from_block 与 to_block 都必填,且必须满足 from_block <= to_block。它比余额接口多几个参数:

  • standard(查询参数,必填):erc20 或 erc721。按地址查询不提供 erc1155,传它会返回 422 no_coverage。
  • direction(查询参数,可选):in、out 或 any,默认 any,按相对该地址的方向过滤。
  • token(查询参数,可选):只返回某个代币合约的转账。
  • clamp(查询参数,可选):仅当取值为字面量字符串 true 时才生效,其他值都按 false 处理。

窗口边界与最终性:显式传入高于 as_of_block 的 to_block 会返回 409 not_indexed_yet,除非 clamp=true 将其截断到 as_of_block;跨度超过链的最大窗口限额(GET /chains 中的 limits.max_window_blocks)的窗口返回 409 window_too_large,除非 clamp=true 从较老的一端截断(抬高 from_block、to_block 不变)。若 from_block 本身已经高于 as_of_block,即使 clamp=true 也仍是硬 409。发生截断或窗口部分覆盖时,响应 meta.coverage 为 "partial",否则为 "full"。

转账记录里,ERC-20 项除公共字段外还有 amount;ERC-721 项有 token_id。两类记录都包含 token、standard、from、to、block_number、block_timestamp、tx_hash、tx_index、log_index。

export BLOCKVECTRA_API_KEY=rgw_your_api_key

# 1) 从余额接口响应中读取 as_of_block。
AS_OF_BLOCK=$(curl -s "https://api.blockvectra.com/v1/data/robinhood_mainnet/addresses/0x1111111111111111111111111111111111111111/balances" \
  -H "x-api-key: $BLOCKVECTRA_API_KEY" | grep -o '"as_of_block":[0-9]*' | cut -d: -f2)

# 2) clamp=true 会把过宽窗口或高于 as_of_block 的 to_block 截断,而不是返回 409。
curl -s "https://api.blockvectra.com/v1/data/robinhood_mainnet/addresses/0x1111111111111111111111111111111111111111/transfers?standard=erc20&from_block=0&to_block=$AS_OF_BLOCK&direction=any&clamp=true" \
  -H "x-api-key: $BLOCKVECTRA_API_KEY"

分页拉全转账

按地址转账接口的 next_cursor 是「乐观」的:仅当这一页恰好返回 limit 条记录时才出现,所以某一页可能带着 next_cursor 却已经是最后一页。不要用「本页是否为空」判断结束,正确做法是循环跟随 next_cursor,直到该字段不存在。

下面的代码把窗口内的转账全部取回:

const address = "0x1111111111111111111111111111111111111111";
const head = await fetch(
  `https://api.blockvectra.com/v1/data/robinhood_mainnet/addresses/${address}/balances`,
  { headers: { "x-api-key": process.env.BLOCKVECTRA_API_KEY! } },
).then((r) => r.json());
const asOfBlock = head.meta.as_of_block;
const transfers: unknown[] = [];
let cursor: string | undefined;

do {
  const url = new URL(
    `https://api.blockvectra.com/v1/data/robinhood_mainnet/addresses/${address}/transfers`,
  );
  url.searchParams.set("standard", "erc20");
  url.searchParams.set("from_block", "0");
  url.searchParams.set("to_block", String(asOfBlock));
  url.searchParams.set("limit", "500");
  // clamp 会从较老一端截断
  url.searchParams.set("clamp", "true");
  if (cursor) url.searchParams.set("cursor", cursor);

  const res = await fetch(url, {
    headers: { "x-api-key": process.env.BLOCKVECTRA_API_KEY! },
  });
  const page = await res.json();
  transfers.push(...page.data);
  cursor = page.next_cursor; // 最后一页不存在该字段
} while (cursor);

请求三:代币元数据与 tokens:batch

单个代币用 GET /{chain}/tokens/{token} 读取,路径只接受 {chain} 与 {token} 两个参数,没有分页。响应信封为 TokenEnvelope,data 是一个 Token:

字段类型说明
addressstring(地址)代币合约地址。
standardstringerc20、erc721 或 unknown。
namestring 或 null代币名称;不可用时为 null。
symbolstring 或 null代币符号;不可用时为 null。
decimalsinteger 或 null代币精度,取值 0–255;不可用时为 null。
total_supplystring 或 null原始总供应量,接口不会套用 decimals 缩放;不可用时为 null。
first_seen_blockinteger(int64)首次出现该代币的区块高度。
metadata_updated_atstring(时间戳)元数据最近一次更新的 UTC 时间。
metadata_blockinteger(int64)读取该代币元数据时的区块高度。
metadata_statusstringok、partial 或 unavailable。
metadata_issuesobject逐字段的问题记录,键为 name、symbol、decimals、total_supply,取值可能为 reverted、no_data、invalid_encoding、temporarily_unavailable。

{chain} 未知或未公开时,先于 token 或请求体校验返回 HTTP 404,附 error.code: not_found 与 error.data.reason: unknown_chain。在已提供服务的链上,{token} 不是合法的 20 字节地址时返回 400 bad_request,不是已知代币时返回 404 not_found。

余额接口会在可用时直接给出 symbol 与 decimals,但这两者都可能是 null。要把钱包里每个代币的名称与精度补齐,用 POST /{chain}/tokens:batch:

  • 请求体为 {"addresses": [...]},一次最多 100 个地址;超过 100 个,或某个条目不是合法的 20 字节地址,都会返回 400 bad_request(遇到第一个非法地址即失败)。
  • 查不到的地址不会触发错误,而是出现在 data.missing 数组里;data.tokens 只包含成功找到元数据的代币。
  • 请求中重复的地址会在 tokens 与 missing 里各自去重,并保持首次出现的请求顺序。
export BLOCKVECTRA_API_KEY=rgw_your_api_key

# 单个代币
curl -s "https://api.blockvectra.com/v1/data/robinhood_mainnet/tokens/0x1111111111111111111111111111111111111111" \
  -H "x-api-key: $BLOCKVECTRA_API_KEY"

# 批量:一次最多 100 个地址
curl -s -X POST "https://api.blockvectra.com/v1/data/robinhood_mainnet/tokens:batch" \
  -H "x-api-key: $BLOCKVECTRA_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"addresses":["0x1111111111111111111111111111111111111111","0x2222222222222222222222222222222222222222"]}'

金额按精度换算

余额字段 balance 与 ERC-20 转账字段 amount 都是十进制字符串表示的原始整数(UInt256String);代币的 total_supply 同样是原始链上整数、不会套用 decimals 缩放。要显示成人可读的数量,需要除以对应代币的 10 ** decimals。

  • decimals 来自余额条目本身的 symbol/decimals,或来自 GET /{chain}/tokens/{token} 与 POST /{chain}/tokens:batch 的元数据;它可能是 null。
  • 这些值可能超过 2^53,不要用 JSON number 直接运算:TypeScript 用 BigInt,Python 用 int,按十进制字符串原样解析,避免精度丢失。
function toDisplayAmount(raw: string, decimals: number | null): string {
  if (decimals === null) return raw; // 没有精度信息时退回原始整数
  const value = BigInt(raw);
  const base = 10n ** BigInt(decimals);
  const whole = value / base;
  const fraction = (value % base)
    .toString()
    .padStart(decimals, "0")
    .replace(/0+$/, "");
  return fraction ? `${whole}.${fraction}` : whole.toString();
}

// balance.balance 是原始十进制字符串,decimals 取自同一条余额或 tokens:batch。
const display = toDisplayAmount(balance.balance, balance.decimals);

数据新鲜度

每个链维度的成功响应都带有 meta:

  • as_of_block:该链最新已完整写入的区块高度。按区块读取的端点提供截至该高度的数据。
  • safe_block:来自节点共识的 safe 区块标记(未知时为 null)。始终不低于 finalized_block,不截断、不拒绝、不延迟响应。
  • finalized_block:来自节点共识的 finalized 区块标记(未知时为 null),仅作为状态标记,不限制返回范围,不截断、不拒绝、不延迟响应;客户端可按需以此判定确认状态。
  • coverage:"full" 或 "partial"。按地址转账等接口在被 clamp 收窄窗口,或窗口起点早于该链首个已索引区块时返回 "partial"。
  • refreshed_at:这份响应背后的数据最近一次更新的 UTC 时间。可能为 null:null 表示这份数据的更新时间未知,应按过期处理(区块类接口始终有值)。
  • 此外还有 chain、chain_slug 与 chain_external_id。

一个常见做法:先用任意一次响应读出 meta.as_of_block 读取到最新已索引区块;若需展示已确认状态,可对比 meta.safe_block 或 meta.finalized_block。

一次页面加载的 CU 估算

每个方法都按其 CU 权重计费,权重从平台计划接口读取:

单次调用的 CU 权重

方法单次调用 CU
data.address_balances25
data.address_transfers25
data.tokens_batch10

一次页面加载(估算)

1 次余额请求 + 3 页转账请求 + 1 次 tokens:batch 请求,共 5 次调用,合计约 110 CU。实际用量取决于翻页次数与代币数量。

计费判定与不计费的错误响应见计费规则。如果你需要的不是已索引的历史转账,而是更实时的节点数据,请先阅读节点近况与已索引历史,再决定是否改用 eth_getLogs。

下一步

最后更新:

本页目录