# API балансов токенов кошелька: активы ERC-20 и история переводов

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

Создайте страницу активов кошелька с помощью API данных блокчейн-кошельков: используйте [API балансов токенов](https://blockvectra.com/ru/data/balances/) для ненулевых остатков ERC-20 и [API переводов токенов](https://blockvectra.com/ru/data/transfers/) для истории кошелька. Разработчики и AI Agent используют одинаковые аутентифицированные запросы. Перед отправкой запросов прочитайте [GET /v1/status](https://api.blockvectra.com/v1/data) и проверьте `data_features` и `data_status` выбранной сети; покрытие балансов различается в зависимости от сети. Параметры запросов и схемы ответов приведены в [справочнике Data API](https://docs.blockvectra.com/ru/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}` считывает название, символ, decimals и общее предложение одного токена по адресу контракта; `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/ru/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`.

Здесь используется keyset-пагинация: `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` (address)  | Адрес контракта токена; каноническая форма — `0x` плюс 40 шестнадцатеричных символов в нижнем регистре.                                                                |
| `balance`  | `string` (decimal)  | Исходный целочисленный баланс, который может превышать `2^53`, возвращаемый в виде обычной десятичной строки — никогда не число JSON, экспоненциальная запись или hex. |
| `symbol`   | `string` or `null`  | Символ токена или `null`, если он недоступен.                                                                                                                          |
| `decimals` | `integer` or `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 из ответа балансов.
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` (address)   | Адрес контракта токена.                                                                                                                                                             |
| `standard`            | `string`             | `erc20`, `erc721` или `unknown`.                                                                                                                                                    |
| `name`                | `string` or `null`   | Название токена или `null`, если оно недоступно.                                                                                                                                    |
| `symbol`              | `string` or `null`   | Символ токена или `null`, если он недоступен.                                                                                                                                       |
| `decimals`            | `integer` or `null`  | Количество десятичных знаков токена, от `0` до `255`, или `null`, если оно недоступно.                                                                                              |
| `total_supply`        | `string` or `null`   | Исходное общее предложение; API не применяет масштабирование по `decimals`. `null`, если недоступно.                                                                                |
| `first_seen_block`    | `integer` (int64)    | Высота блока, в котором токен был впервые обнаружен.                                                                                                                                |
| `metadata_updated_at` | `string` (timestamp) | Время 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`. Чтобы заполнить название и decimals для каждого токена в кошельке, используйте `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

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


  **TypeScript**

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

// Batch: group by 100 addresses to enrich the tokens from the balances response
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 token
single = requests.get(
    "https://api.blockvectra.com/v1/data/robinhood_mainnet/tokens/0x1111111111111111111111111111111111111111",
    headers=headers,
).json()
print(single["data"])

# Batch: group by 100 addresses to enrich the tokens from the balances response
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"])
```


## Масштабирование сумм с учетом decimals

Поле баланса `balance` и поле перевода ERC-20 `amount` представляют собой исходные целые числа, представленные в виде десятичных строк (`UInt256String`); значение `total_supply` токена также является исходным ончейн-целым числом без применения масштабирования по `decimals`. Чтобы отобразить понятное человеку количество, разделите значение на `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; // 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);
```


  **Python**

```python
from decimal import Decimal


def to_display_amount(raw: str, decimals: int | None) -> str:
    if decimals is None:
        return raw  # no decimals metadata: keep the raw integer
    value = Decimal(raw)  # parse the decimal string exactly
    return format(value.scaleb(-decimals).normalize(), "f")


# balance["balance"] is a raw decimal string; decimals comes from the same item or 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/ru/guides/billing-rules/). Если вам требуется не проиндексированная история переводов, а логи из самых последних блоков, сначала прочтите руководство [Свежие данные ноды или проиндексированная история](https://docs.blockvectra.com/ru/guides/logs-vs-transfers/), прежде чем принимать решение о переходе на `eth_getLogs`.

## Следующие шаги

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