# API балансів токенів гаманця: активи ERC-20 та історія переказів

> Source: https://docs.blockvectra.com/uk/guides/wallet-assets/

Створіть сторінку активів гаманця за допомогою API блокчейн-даних гаманця: використовуйте [API балансів токенів](https://blockvectra.com/en/data/balances/) для ненульових активів ERC-20 та [API переказів токенів](https://blockvectra.com/en/data/transfers/) для історії гаманця. Розробники та AI-агенти використовують ті самі автентифіковані запити. Перед виконанням запитів виконайте [GET /v1/status](https://api.blockvectra.com/v1/data) та перевірте `data_features` і `data_status` обраної мережі; покриття балансів відрізняється залежно від мережі. Параметри запитів і схеми відповідей наведено в [довіднику Data API](https://docs.blockvectra.com/en/api/data/).

## Завдання, які допомагає вирішити цей посібник

* [Зчитувати баланси токенів гаманця](#request-1-address-balances) за допомогою ключа та пагінувати ненульові активи ERC-20.
* [Зчитувати історію переказів гаманця](#request-2-address-transfers) у межах фіксованого вікна блоків та переходити за курсорами для обраної адреси.
* [Заповнювати метадані токенів](#request-3-token-metadata-and-tokensbatch), щоб відображати назви та символи поруч із необробленими цілочисельними балансами, зберігаючи відсутні поля.

## Три типи даних, необхідні сторінці активів гаманця

Сторінка активів гаманця може відображати баланси токенів 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` як базовий URL і заголовок запиту `x-api-key`, де `robinhood_mainnet` наведено як приклад мережі. Вони належать відповідно до можливостей `balances`, `transfers` та `token_metadata`; перелік мереж, що надають кожну можливість, дивіться на сторінці [Підтримувані мережі](https://docs.blockvectra.com/en/chains/). У мережі без відповідної можливості ендпоінт повертає `422 no_coverage`.

## Запит 1: баланси адреси

Цей ендпоінт приймає небагато параметрів, що робить його чудовим першим запитом для сторінки:

* `{chain}` (параметр шляху, обов'язковий): ідентифікатор мережі, значення `chain` запису в `GET /chains` (наприклад, `robinhood_mainnet`). Зіставлення точне та чутливе до регістру; псевдоніми та числові Chain ID не приймаються.
* `{address}` (параметр шляху, обов'язковий): 20-байтна адреса; префікс `0x` необов'язковий, підтримується будь-який регістр.
* `limit` (параметр запиту, необов'язковий): розмір сторінки. За замовчуванням — 50; значення понад 500 обмежуються до 500; `0` або неціле число повертає `400 bad_request`.
* `cursor` (параметр запиту, необов'язковий): `next_cursor` з попередньої відповіді, переданий назад без змін для отримання наступної сторінки. Курсор дійсний лише для тієї мережі, ендпоінта та параметрів запиту, які його видали; повторне використання в іншому місці повертає `400 bad_request`.

Він використовує пагінацію за набором ключів: `next_cursor` з'являється лише тоді, коли існує наступна сторінка. На останній сторінці ключ взагалі відсутній, а не `null`.

**cURL**

```bash
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"
```


  **TypeScript**

```ts
const address = "0x1111111111111111111111111111111111111111";
const url = new URL(
  `https://api.blockvectra.com/v1/data/robinhood_mainnet/addresses/${address}/balances`,
);
url.searchParams.set("limit", "50");

const res = await fetch(url, {
  headers: { "x-api-key": process.env.BLOCKVECTRA_API_KEY! },
});
const balanceBody = await res.json();
console.log(balanceBody.data, balanceBody.meta);

// npx tsx example.mts
```


  **Python**

```python
import os
import requests

address = "0x1111111111111111111111111111111111111111"
res = requests.get(
    f"https://api.blockvectra.com/v1/data/robinhood_mainnet/addresses/{address}/balances",
    params={"limit": 50},
    headers={"x-api-key": os.environ["BLOCKVECTRA_API_KEY"]},
)
res.raise_for_status()
balance_body = res.json()
print(balance_body["data"], balance_body["meta"])
```


Обгорткою відповіді є `AddressBalanceListEnvelope`, що містить `data` та `meta`. Кожен елемент `data` є об'єктом `AddressBalance`:

| Поле       | Тип                   | Опис                                                                                                                                                                       |
| ---------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `token`    | `string` (адреса)     | Адреса контракту токена; канонічною формою є `0x` плюс 40 малих шістнадцяткових цифр.                                                                                      |
| `balance`  | `string` (десятковий) | Необроблений цілочисельний баланс, який може перевищувати `2^53`, повертається як звичайний десятковий рядок — ніколи не як число JSON, наукова чи шістнадцяткова нотація. |
| `symbol`   | `string` або `null`   | Символ токена, або `null`, якщо недоступно.                                                                                                                                |
| `decimals` | `integer` або `null`  | Кількість десяткових знаків токена, `0`–`255`, або `null`, якщо недоступно.                                                                                                |

## Запит 2: перекази адреси

Ендпоінт переказів вимагає явного вікна блоків: `from_block` і `to_block` є обов'язковими та мають задовольняти умову `from_block <= to_block`. Він приймає ще кілька параметрів:

* `standard` (параметр запиту, обов'язковий): `erc20` або `erc721`. Запити на рівні адреси не покривають `erc1155`; передача цього значення повертає `422 no_coverage`.
* `direction` (параметр запиту, необов'язковий): `in`, `out` або `any`; за замовчуванням — `any`, фільтрує за напрямком відносно адреси.
* `token` (параметр запиту, необов'язковий): обмежує результати одним контрактом токена.
* `clamp` (параметр запиту, необов'язковий): лише точний рядок `true` активує його; будь-яке інше значення вважається `false`.

Межі вікна та фінальність: явний `to_block`, вищий за `as_of_block`, повертає `409 not_indexed_yet`, якщо тільки `clamp=true` не скоротить його до `as_of_block`; вікно, ширше за ліміт мережі (`limits.max_window_blocks` із `GET /chains`), повертає `409 window_too_large`, якщо тільки `clamp=true` не скоротить його зі старішого кінця (збільшуючи `from_block` і залишаючи `to_block` фіксованим). Якщо сам `from_block` уже перевищує `as_of_block`, запит залишається суворою помилкою `409` навіть із `clamp=true`. Коли вікно обрізано або частково покрите, поле `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`.

**cURL**

```bash
export BLOCKVECTRA_API_KEY=rgw_your_api_key

# 1) Зчитайте as_of_block із відповіді balances.
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 скорочує занадто широке вікно або to_block вище as_of_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"
```


  **TypeScript**

```ts
const address = "0x1111111111111111111111111111111111111111";

// 1) Зчитайте as_of_block із meta будь-якої попередньої відповіді.
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());

// 2) Використовуйте as_of_block як верхню межу вікна переказів.
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(head.meta.as_of_block));
url.searchParams.set("direction", "any");
url.searchParams.set("clamp", "true");

const res = await fetch(url, {
  headers: { "x-api-key": process.env.BLOCKVECTRA_API_KEY! },
});
const body = await res.json();
console.log(body.data, body.meta);

// npx tsx example.mts
```


  **Python**

```python
import os
import requests

address = "0x1111111111111111111111111111111111111111"
headers = {"x-api-key": os.environ["BLOCKVECTRA_API_KEY"]}

# 1) Зчитайте as_of_block із meta будь-якої попередньої відповіді.
head = requests.get(
    f"https://api.blockvectra.com/v1/data/robinhood_mainnet/addresses/{address}/balances",
    headers=headers,
).json()

# 2) Використовуйте as_of_block як верхню межу вікна переказів.
res = requests.get(
    f"https://api.blockvectra.com/v1/data/robinhood_mainnet/addresses/{address}/transfers",
    params={
        "standard": "erc20",
        "from_block": 0,
        "to_block": head["meta"]["as_of_block"],
        "direction": "any",
        "clamp": "true",
    },
    headers=headers,
)
res.raise_for_status()
body = res.json()
print(body["data"], body["meta"])
```


## Пагінація всіх переказів

Поле `next_cursor` ендпоінта переказів адреси є оптимістичним: воно з'являється лише тоді, коли сторінка повернула рівно `limit` рядків, тому сторінка може містити `next_cursor` і все одно виявитися останньою. Не зупиняйтеся, коли сторінка порожня; слідуйте за `next_cursor`, доки ключ не зникне.

Наведений нижче код отримує кожен переказ у вікні:

**TypeScript**

```ts
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);
```


  **Python**

```python
import os
import requests

address = "0x1111111111111111111111111111111111111111"
head = requests.get(
    f"https://api.blockvectra.com/v1/data/robinhood_mainnet/addresses/{address}/balances",
    headers={"x-api-key": os.environ["BLOCKVECTRA_API_KEY"]},
).json()
as_of_block = head["meta"]["as_of_block"]
transfers = []
cursor = None

while True:
    params = {
        "standard": "erc20",
        "from_block": 0,
        "to_block": as_of_block,
        "limit": 500,
        # clamp обрізає зі старішого кінця
        "clamp": "true",
    }
    if cursor:
        params["cursor"] = cursor
    res = requests.get(
        f"https://api.blockvectra.com/v1/data/robinhood_mainnet/addresses/{address}/transfers",
        params=params,
        headers={"x-api-key": os.environ["BLOCKVECTRA_API_KEY"]},
    )
    res.raise_for_status()
    page = res.json()
    transfers.extend(page["data"])
    cursor = page.get("next_cursor")  # відсутнє на останній сторінці
    if not cursor:
        break
```


## Запит 3: метадані токенів та 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`     | Необроблена загальна пропозиція; API не застосовує масштабування `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`. |

Значення `{token}`, яке не є дійсною 20-байтною адресою, повертає `400 bad_request`; невідомий `{token}` повертає `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`, кожна в порядку першої появи в запиті.

**cURL**

```bash
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"]}'
```


  **TypeScript**

```ts
// Один токен
const single = await fetch(
  "https://api.blockvectra.com/v1/data/robinhood_mainnet/tokens/0x1111111111111111111111111111111111111111",
  { headers: { "x-api-key": process.env.BLOCKVECTRA_API_KEY! } },
).then((r) => r.json());
console.log(single.data);

// Пакет: групуйте по 100 адрес для збагачення токенів із відповіді балансів
const BATCH_SIZE = 100;
const addresses = balanceBody.data.map((item: { token: string }) => item.token);
const tokens = new Map<string, unknown>();
const missing: string[] = [];

for (let i = 0; i < addresses.length; i += BATCH_SIZE) {
  const res = await fetch("https://api.blockvectra.com/v1/data/robinhood_mainnet/tokens:batch", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "x-api-key": process.env.BLOCKVECTRA_API_KEY!,
    },
    body: JSON.stringify({ addresses: addresses.slice(i, i + BATCH_SIZE) }),
  });
  const body = await res.json();
  for (const token of body.data.tokens) tokens.set(token.address, token);
  missing.push(...body.data.missing);
}

// npx tsx example.mts
```


  **Python**

```python
import os
import requests

headers = {"x-api-key": os.environ["BLOCKVECTRA_API_KEY"]}

# Один токен
single = requests.get(
    "https://api.blockvectra.com/v1/data/robinhood_mainnet/tokens/0x1111111111111111111111111111111111111111",
    headers=headers,
).json()
print(single["data"])

# Пакет: групуйте по 100 адрес для збагачення токенів із відповіді балансів
BATCH_SIZE = 100
addresses = [item["token"] for item in balance_body["data"]]
tokens = {}
missing = []

for i in range(0, len(addresses), BATCH_SIZE):
    res = requests.post(
        "https://api.blockvectra.com/v1/data/robinhood_mainnet/tokens:batch",
        json={"addresses": addresses[i : i + BATCH_SIZE]},
        headers={**headers, "Content-Type": "application/json"},
    )
    res.raise_for_status()
    body = res.json()
    for token in body["data"]["tokens"]:
        tokens[token["address"]] = token
    missing.extend(body["data"]["missing"])
```


## Масштабування сум за кількістю десяткових знаків

Поле балансу `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: використовуйте `BigInt` у TypeScript та `Decimal` у Python, парсячи десятковий рядок як є, щоб уникнути втрати точності.

**TypeScript**

```ts
function toDisplayAmount(raw: string, decimals: number | null): string {
  if (decimals === null) return raw; // метадані decimals відсутні: зберігаємо необроблене ціле число
  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);
```


  **Python**

```python
from decimal import Decimal


def to_display_amount(raw: str, decimals: int | None) -> str:
    if decimals is None:
        return raw  # метадані decimals відсутні: зберігаємо необроблене ціле число
    value = Decimal(raw)  # точно парсимо десятковий рядок
    return format(value.scaleb(-decimals).normalize(), "f")


# balance["balance"] є необробленим десятковим рядком; decimals надходить із того самого елемента або tokens:batch.
display = to_display_amount(balance["balance"], balance["decimals"])
```


## Свіжість даних

Кожна успішна відповідь у межах мережі містить `meta`:

* `as_of_block`: найновіший повністю записаний блок мережі. Ендпоінти рівня блоків обслуговують дані до цієї висоти.
* `safe_block`: маркер, що вказує на консенсусний тег блоку `safe` вузла (`null`, доки невідомо). Ніколи не буває нижчим за `finalized_block`, не скорочує, не відхиляє та не затримує відповіді.
* `finalized_block`: маркер, що вказує на консенсусний тег блоку `finalized` вузла (`null`, доки невідомо). Він не скорочує, не відхиляє та не затримує відповіді; клієнти самі вирішують, який рівень безпеки їм потрібен від маркера (наприклад, статус підтвердження).
* `coverage`: `"full"` або `"partial"`. Перекази адрес та подібні ендпоінти повідомляють про `"partial"`, коли `clamp` звузив обслуговуване вікно або коли вікно починається до першого проіндексованого блоку мережі.
* `refreshed_at`: коли дані у відповіді оновлювалися востаннє (UTC). Може бути `null`: `null` означає, що час оновлення даних невідомий і їх слід вважати застарілими; ендпоінти на основі блоків завжди повертають значення.
* Також повторюються поля `chain`, `chain_slug` та `chain_external_id`.

Поширений шаблон: прочитайте `meta.as_of_block` із будь-якої першої відповіді, щоб зчитувати дані до найновішого проіндексованого блоку, та перевірте `meta.safe_block` / `meta.finalized_block`, якщо бажаєте відображати статус підтвердження.

## Оцінка CU для одного завантаження сторінки

Кожен метод тарифікується за його вагою в CU, зчитаною з API тарифних планів платформи:

**Вага CU на виклик**

| Метод | CU на виклик |
| --- | --- |
| `data.address_balances` | 25 |
| `data.address_transfers` | 25 |
| `data.tokens_batch` | 10 |

**Одне завантаження сторінки (оцінка)**

1 запит balances + 3 сторінок transfer + 1 запит(ів) `tokens:batch`, загалом 5 викликів, близько 110 CU. Фактичне використання залежить від кількості сторінок і токенів.

Щодо рішень про білінг та незатарифікованих відповідей із помилками звертайтеся до [правил білінгу](https://docs.blockvectra.com/en/guides/billing-rules/). Якщо вам потрібна не індексована історія переказів, а логи з найновіших блоків, спершу прочитайте статтю [Нещодавні дані вузла проти індексованої історії](https://docs.blockvectra.com/en/guides/logs-vs-transfers/), перш ніж вирішувати, чи переходити на `eth_getLogs`.

## Наступні кроки

* [Перегляньте каталог наборів даних](https://blockvectra.com/en/data/), щоб ознайомитися з усіма наборами даних, які індексує BlockVectra.
* [Перегляньте безкоштовний план і ціни](https://blockvectra.com/en/pricing/#free), щоб дізнатися, що включено у ваш акаунт.
* [Увійдіть до консолі](https://console.blockvectra.com/login/?next=%2Fkeys%2F), щоб створити API key.
