钱包代币余额 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 参考。
这篇帮你完成什么任务
- 读取钱包代币余额:携带 key 调用钱包数据 API,分页获取非零 ERC-20 持仓。
- 读取钱包转账历史:限定区块窗口,按游标读取所选地址的转账记录。
- 补齐代币元数据:为原始整数余额配上名称与符号,保留缺失字段。
钱包资产页需要哪三类数据
钱包资产页可以展示地址的 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:
| 字段 | 类型 | 说明 |
|---|---|---|
token | string(地址) | 代币合约地址,规范形式为 0x 加 40 位小写十六进制字符。 |
balance | string(十进制) | 原始整数余额,可能超过 2^53,以纯十进制字符串返回,绝不使用 JSON number、科学计数法或十六进制。 |
symbol | string 或 null | 代币符号;不可用时为 null。 |
decimals | integer 或 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:
| 字段 | 类型 | 说明 |
|---|---|---|
address | string(地址) | 代币合约地址。 |
standard | string | erc20、erc721 或 unknown。 |
name | string 或 null | 代币名称;不可用时为 null。 |
symbol | string 或 null | 代币符号;不可用时为 null。 |
decimals | integer 或 null | 代币精度,取值 0–255;不可用时为 null。 |
total_supply | string 或 null | 原始总供应量,接口不会套用 decimals 缩放;不可用时为 null。 |
first_seen_block | integer(int64) | 首次出现该代币的区块高度。 |
metadata_updated_at | string(时间戳) | 元数据最近一次更新的 UTC 时间。 |
metadata_block | integer(int64) | 读取该代币元数据时的区块高度。 |
metadata_status | string | ok、partial 或 unavailable。 |
metadata_issues | object | 逐字段的问题记录,键为 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_balances | 25 |
data.address_transfers | 25 |
data.tokens_batch | 10 |
一次页面加载(估算)
1 次余额请求 + 3 页转账请求 + 1 次 tokens:batch 请求,共 5 次调用,合计约 110 CU。实际用量取决于翻页次数与代币数量。
计费判定与不计费的错误响应见计费规则。如果你需要的不是已索引的历史转账,而是更实时的节点数据,请先阅读节点近况与已索引历史,再决定是否改用 eth_getLogs。
下一步
最后更新: