API số dư token trong ví: tài sản ERC-20 và lịch sử chuyển tiền

Xây dựng trang tài sản ví với số dư token ERC-20 khác 0, lịch sử chuyển token và siêu dữ liệu theo lô. Kiểm tra phạm vi chuỗi, phân trang kết quả và quy đổi số tiền nguyên theo decimals.

Xây dựng trang tài sản ví bằng API dữ liệu ví blockchain: dùng Token Balances API cho lượng ERC-20 nắm giữ khác 0 và Token Transfers API cho lịch sử ví. Nhà phát triển và AI Agent dùng cùng các yêu cầu có xác thực. Trước khi truy vấn, đọc GET /v1/status và kiểm tra data_features cùng data_status của chuỗi đã chọn; phạm vi số dư khác nhau theo chuỗi. Tham số yêu cầu và lược đồ phản hồi nằm trong tham chiếu Data API.

Các tác vụ hướng dẫn này giúp bạn hoàn thành

Ba loại dữ liệu trang tài sản ví cần

Trang tài sản ví có thể hiển thị số dư token ERC-20, lịch sử chuyển token và siêu dữ liệu token của một địa chỉ. Data API cung cấp endpoint cho từng loại:

  • Số dư: GET /{chain}/addresses/{address}/balances trả số dư ERC-20 khác 0 của địa chỉ, sắp xếp tăng dần theo địa chỉ token, kèm symbol và decimals của token khi có. Địa chỉ không có số dư trả 200 với data: [].
  • Chuyển tiền: GET /{chain}/addresses/{address}/transfers trả các chuyển token liên quan tới địa chỉ trong cửa sổ khối bắt buộc, sắp xếp giảm dần theo (block_number, log_index).
  • Siêu dữ liệu token: GET /{chain}/tokens/{token} đọc tên, ký hiệu, số chữ số thập phân và tổng cung của một token theo địa chỉ hợp đồng; POST /{chain}/tokens:batch đọc cùng siêu dữ liệu cho tối đa 100 địa chỉ trong một yêu cầu.

Cả ba dùng https://api.blockvectra.com/v1/data làm URL cơ sở và header yêu cầu x-api-key, với robinhood_mainnet là chuỗi ví dụ. Chúng lần lượt thuộc các khả năng balances, transfers và token_metadata; để xem chuỗi cung cấp từng khả năng, đọc trang Chuỗi được hỗ trợ. Trên chuỗi không có khả năng tương ứng, endpoint trả 422 no_coverage.

Yêu cầu 1: số dư địa chỉ

Endpoint này cần ít tham số hơn, nên phù hợp làm yêu cầu đầu tiên cho trang:

  • {chain} (tham số đường dẫn, bắt buộc): định danh chuỗi, là giá trị chain của một mục trong GET /chains (ví dụ robinhood_mainnet). Khớp chính xác và phân biệt chữ hoa, chữ thường; không chấp nhận bí danh hoặc Chain ID dạng số.
  • {address} (tham số đường dẫn, bắt buộc): địa chỉ 20 byte; tiền tố 0x là tùy chọn và chấp nhận cả chữ hoa lẫn chữ thường.
  • limit (tham số truy vấn, tùy chọn): kích thước trang. Mặc định 50; giá trị trên 500 được giới hạn thành 500; 0 hoặc giá trị không nguyên trả 400 bad_request.
  • cursor (tham số truy vấn, tùy chọn): next_cursor của phản hồi trước, truyền lại nguyên vẹn để lấy trang tiếp theo. Con trỏ chỉ hợp lệ cho chuỗi, endpoint và tham số truy vấn đã tạo ra nó; dùng lại ở nơi khác trả 400 bad_request.

Endpoint phân trang theo khóa: next_cursor chỉ xuất hiện khi có trang tiếp theo. Ở trang cuối, khóa hoàn toàn không có, tuyệt đối không phải 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"

Cấu trúc phản hồi là AddressBalanceListEnvelope, chứa data và meta. Mỗi mục trong data là một AddressBalance:

TrườngKiểuMô tả
tokenstring (địa chỉ)Địa chỉ hợp đồng token; dạng chuẩn là 0x cộng 40 chữ số hex viết thường.
balancestring (thập phân)Số dư nguyên thô, có thể vượt 2^53, trả dưới dạng chuỗi thập phân thuần — tuyệt đối không phải số JSON, ký pháp khoa học hay hex.
symbolstring hoặc nullKý hiệu token, hoặc null khi không có.
decimalsinteger hoặc nullSố chữ số thập phân của token, 0–255, hoặc null khi không có.

Yêu cầu 2: chuyển tiền của địa chỉ

Endpoint chuyển tiền yêu cầu cửa sổ khối tường minh: cả from_block và to_block đều bắt buộc và phải thỏa from_block <= to_block. Endpoint cần thêm vài tham số:

  • standard (tham số truy vấn, bắt buộc): erc20 hoặc erc721. Truy vấn theo địa chỉ không bao phủ erc1155; truyền giá trị đó trả 422 no_coverage.
  • direction (tham số truy vấn, tùy chọn): in, out hoặc any; mặc định any và lọc theo hướng tương đối với địa chỉ.
  • token (tham số truy vấn, tùy chọn): giới hạn kết quả ở một hợp đồng token.
  • clamp (tham số truy vấn, tùy chọn): chỉ chuỗi nguyên văn true mới bật tùy chọn này; mọi giá trị khác được coi là false.

Giới hạn cửa sổ và tính chung cuộc: to_block tường minh vượt as_of_block trả 409 not_indexed_yet, trừ khi clamp=true cắt xuống as_of_block; cửa sổ rộng hơn giới hạn của chuỗi (limits.max_window_blocks từ GET /chains) trả 409 window_too_large, trừ khi clamp=true cắt từ đầu cũ hơn (tăng from_block và giữ nguyên to_block). Nếu bản thân from_block đã vượt as_of_block, vẫn trả lỗi 409 ngay cả với clamp=true. Khi cửa sổ bị cắt hoặc chỉ được bao phủ một phần, meta.coverage trong phản hồi là "partial"; nếu không là "full".

Trong bản ghi chuyển tiền, mục ERC-20 thêm amount; mục ERC-721 thêm token_id. Cả hai đều có token, standard, from, to, block_number, block_timestamp, tx_hash, tx_index và log_index.

export BLOCKVECTRA_API_KEY=rgw_your_api_key

# 1) Read as_of_block from the balances response.
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 truncates a too-wide window, or a to_block above as_of_block, instead of returning 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"

Phân trang qua toàn bộ chuyển tiền

next_cursor của endpoint chuyển tiền theo địa chỉ mang tính dự đoán: chỉ xuất hiện khi trang trả đúng limit hàng, nên trang có thể có next_cursor nhưng vẫn là trang cuối. Đừng dừng khi trang rỗng; theo next_cursor cho đến khi khóa không còn.

Mã bên dưới lấy mọi chuyển tiền trong cửa sổ:

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 truncates from the older end
  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; // absent on the last page
} while (cursor);

Yêu cầu 3: siêu dữ liệu token và tokens:batch

Đọc một token bằng GET /{chain}/tokens/{token}; đường dẫn chỉ nhận {chain} và {token}, không phân trang. Cấu trúc phản hồi là TokenEnvelope, và data là một Token:

TrườngKiểuMô tả
addressstring (địa chỉ)Địa chỉ hợp đồng token.
standardstringerc20, erc721 hoặc unknown.
namestring hoặc nullTên token, hoặc null khi không có.
symbolstring hoặc nullKý hiệu token, hoặc null khi không có.
decimalsinteger hoặc nullSố chữ số thập phân của token, 0–255, hoặc null khi không có.
total_supplystring hoặc nullTổng cung thô; API không quy đổi theo decimals. null khi không có.
first_seen_blockinteger (int64)Độ cao khối nơi token được thấy lần đầu.
metadata_updated_atstring (dấu thời gian)Thời gian UTC siêu dữ liệu được cập nhật lần cuối.
metadata_blockinteger (int64)Độ cao khối tại đó siêu dữ liệu được đọc.
metadata_statusstringok, partial hoặc unavailable.
metadata_issuesobjectBản ghi vấn đề theo từng trường, với khóa name, symbol, decimals, total_supply và giá trị reverted, no_data, invalid_encoding hoặc temporarily_unavailable.

{token} không phải địa chỉ 20 byte hợp lệ trả 400 bad_request; {token} không được biết đến trả 404 not_found; {chain} không được biết đến trả 404 unknown_chain.

Endpoint số dư đã bao gồm symbol và decimals khi có, nhưng cả hai có thể là null. Để bổ sung tên và số chữ số thập phân cho mọi token trong ví, dùng POST /{chain}/tokens:batch:

  • Body yêu cầu là {"addresses": [...]} với tối đa 100 địa chỉ mỗi yêu cầu; hơn 100 mục, hoặc một mục không phải địa chỉ 20 byte hợp lệ, trả 400 bad_request (thất bại tại địa chỉ không hợp lệ đầu tiên được duyệt tới).
  • Địa chỉ không tìm thấy không gây lỗi; chúng nằm trong data.missing, còn data.tokens chỉ chứa token có siêu dữ liệu tìm thấy.
  • Địa chỉ trùng được loại trùng trong cả tokens và missing, mỗi danh sách theo thứ tự xuất hiện đầu tiên trong yêu cầu.
export BLOCKVECTRA_API_KEY=rgw_your_api_key

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

# Batch: up to 100 addresses per request
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"]}'

Quy đổi số tiền theo decimals

Trường số dư balance và trường chuyển tiền ERC-20 amount là số nguyên thô biểu diễn bằng chuỗi thập phân (UInt256String); total_supply của token cũng là số nguyên thô trên chuỗi chưa quy đổi theo decimals. Để hiển thị số lượng dễ đọc, quy đổi theo decimals của token đó.

  • decimals lấy từ symbol/decimals của chính mục số dư, hoặc từ GET /{chain}/tokens/{token} và POST /{chain}/tokens:batch; có thể là null.
  • Các giá trị này có thể vượt 2^53, nên đừng tính toán bằng số JSON: dùng BigInt trong TypeScript và Decimal trong Python, phân tích nguyên chuỗi thập phân để tránh mất độ chính xác.
function toDisplayAmount(raw: string, decimals: number | null): string {
  if (decimals === null) return raw; // no decimals metadata: keep the raw integer
  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 is a raw decimal string; decimals comes from the same item or tokens:batch.
const display = toDisplayAmount(balance.balance, balance.decimals);

Độ mới của dữ liệu

Mọi phản hồi thành công trong phạm vi chuỗi đều có meta:

  • as_of_block: khối mới nhất của chuỗi đã được ghi đầy đủ. Endpoint theo khối phục vụ dữ liệu đến độ cao này.
  • safe_block: mốc chỉ ra block tag đồng thuận safe của nút (null khi chưa biết). Không bao giờ thấp hơn finalized_block, và không cắt, từ chối hoặc trì hoãn phản hồi.
  • finalized_block: mốc chỉ ra block tag đồng thuận finalized của nút (null khi chưa biết). Không cắt, từ chối hoặc trì hoãn phản hồi; client quyết định mức an toàn cần từ mốc đó (chẳng hạn trạng thái xác nhận).
  • coverage: "full" hoặc "partial". Chuyển tiền theo địa chỉ và endpoint tương tự báo "partial" khi clamp thu hẹp cửa sổ được phục vụ, hoặc khi cửa sổ bắt đầu trước khối đầu tiên được lập chỉ mục của chuỗi.
  • refreshed_at: thời điểm dữ liệu của phản hồi được cập nhật lần cuối (UTC). Có thể là null: null nghĩa là chưa biết thời gian cập nhật dữ liệu và cần coi dữ liệu là cũ; endpoint theo khối luôn trả giá trị.
  • Phản hồi cũng lặp lại chain, chain_slug và chain_external_id.

Cách dùng phổ biến: đọc meta.as_of_block từ phản hồi đầu tiên bất kỳ để đọc đến khối mới nhất đã lập chỉ mục, và kiểm tra meta.safe_block / meta.finalized_block nếu muốn hiển thị trạng thái đã xác nhận.

Ước tính CU cho một lần tải trang

Mỗi phương thức được tính phí theo trọng số CU, đọc từ API gói của nền tảng:

Trọng số CU mỗi lệnh gọi

Phương thứcCU mỗi lệnh gọi
data.address_balances25
data.address_transfers25
data.tokens_batch10

Một lần tải trang (ước tính)

1 yêu cầu balances + 3 trang transfer + 1 yêu cầu tokens:batch, tổng cộng 5 lệnh gọi, khoảng 110 CU. Mức sử dụng thực tế phụ thuộc vào số lượng trang và token.

Để quyết định tính phí và biết phản hồi lỗi không tính phí, xem quy tắc thanh toán. Nếu cần log từ các khối mới nhất thay vì lịch sử chuyển tiền được lập chỉ mục, đọc Dữ liệu nút gần đây và lịch sử được lập chỉ mục trước khi quyết định chuyển sang eth_getLogs.

Các bước tiếp theo

Cập nhật lần cuối:

Trên trang này