当前活动:每个账户有 1 次重置机会,余额用完可一键补回到 3,000 万 CU;新用户注册即得 3,000 万 CU。 了解详情 →
指南

用 Data API 做钱包资产页:余额、转账与代币信息

使用 Data API 余额、代币转账与元数据接口搭建钱包资产页。掌握三种核心请求构造、基于游标分页获取完整转账记录、批量补齐代币符号精度与数据新鲜度处理。

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

一个钱包资产页通常要回答三个问题:这个地址现在持有什么、它发生过哪些代币转账、每个代币叫什么、精度是多少。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://dev-api.blockvectra.network/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://dev-api.blockvectra.network/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 处理。

窗口边界与最终性:显式传入高于 finalized_block 的 to_block 会返回 409 finality_exceeded,除非 clamp=true 将其截断到 finalized_block;跨度超过 100,000 个区块的窗口返回 409 window_too_large,除非 clamp=true 从较老的一端截断(抬高 from_block、to_block 不变)。若 from_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

# 先用任意一次响应读出 meta.finalized_block,把它作为窗口上界。
# clamp=true 会把过宽窗口或高于 finalized_block 的 to_block 截断,而不是返回 409。
curl -s "https://dev-api.blockvectra.network/v1/data/robinhood_mainnet/addresses/0x1111111111111111111111111111111111111111/transfers?standard=erc20&from_block=0&to_block=$FINALIZED_BLOCK&direction=any&clamp=true" \
  -H "x-api-key: $BLOCKVECTRA_API_KEY"

分页拉全转账

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

  • limit 缺省 50,最大 500。
  • 传回 cursor 时保持原值不变;游标只对签发它的链、端点和查询参数有效,换一条链或改动参数都要重新从第一页开始。
  • 游标本身携带区块位置:如果翻页期间链的首个已索引区块向前移动,下一页会变成 partial(低于该位置的记录不再返回)或 422 no_coverage。

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

const address = "0x1111111111111111111111111111111111111111";
const finalizedBlock = head.meta.finalized_block;
const transfers: unknown[] = [];
let cursor: string | undefined;

do {
  const url = new URL(
    `https://dev-api.blockvectra.network/v1/data/robinhood_mainnet/addresses/${address}/transfers`,
  );
  url.searchParams.set("standard", "erc20");
  url.searchParams.set("from_block", "0");
  url.searchParams.set("to_block", String(finalizedBlock));
  url.searchParams.set("limit", "500");
  // 窗口超过规格上限时返回 409 window_too_large,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。

{token} 不是合法的 20 字节地址时返回 400 bad_request;不是已知代币时返回 404 not_found;{chain} 未知时返回 404 unknown_chain。

余额接口会在可用时直接给出 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://dev-api.blockvectra.network/v1/data/robinhood_mainnet/tokens/0x1111111111111111111111111111111111111111" \
  -H "x-api-key: $BLOCKVECTRA_API_KEY"

# 批量:一次最多 100 个地址
curl -s -X POST "https://dev-api.blockvectra.network/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 缩放。要显示成人可读的数量,需要用对应代币的 decimals 做除法。

  • decimals 来自余额条目本身的 symbol/decimals,或来自 GET /{chain}/tokens/{token} 与 POST /{chain}/tokens:batch 的元数据;它可能是 null。
  • 这些值可能超过 2^53,不要用 JSON number 直接运算:TypeScript 用 BigInt,Python 用 Decimal,按十进制字符串原样解析,避免精度丢失。
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:计算该响应最终性水位时所依据的已索引头部区块高度。
  • finalized_block:按区块读取的端点会提供的最高区块高度;它按链以固定区块数落后于 as_of_block,是重组安全水位线,而不是共识最终性信号。
  • coverage:"full" 或 "partial"。按地址转账等接口在被 clamp 收窄窗口,或窗口起点早于该链首个已索引区块时返回 "partial"。
  • refreshed_at:这份响应背后的数据最近一次更新的 UTC 时间。
  • 此外还有 chain、chain_slug 与 chain_external_id。

余额与代币元数据这类没有天然区块作用域的快照/元数据接口同样会返回 as_of_block 与 finalized_block,但不会拿请求去和它们比较。转账接口只提供不高于 finalized_block 的数据。

一个常见做法:先用任意一次响应读出 meta.finalized_block,把它作为转账窗口的 to_block,就不必手写区块高度。

一次页面加载的 CU 估算

每个方法都按其 CU 权重计费,权重在构建时从平台计划接口读取,下文不写死具体数字:

单次调用的 CU 权重

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

以上权重在构建时从平台计划接口读取。

一次页面加载(估算)

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

计费判定与不计费的错误响应见计费规则。如果你需要的不是已索引的历史转账,而是最新区块里尚未跨过最终性水位线的日志,请先阅读节点近况与已索引历史,再决定是否改用 eth_getLogs。

下一步

本页目录