# Як відстежувати платежі в USDT / USDC за допомогою Webhook та RPC

> Source: https://docs.blockvectra.com/uk/guides/stablecoin-payments/

Для моніторингу платежів у стейблкоїнах або виявлення депозитів на біржі відстежуйте вхідні перекази ERC-20 USDT / USDC в EVM-мережах за допомогою Webhook, логів WebSocket або HTTP-опитування. Розробники та AI-агенти використовують ті самі API; оберіть мережу, контракт токена, одержувача та глибину підтвердження перед обробкою платежів. Оберіть робочий процес для депозитів, сповіщень продавців або виплат у [рішенні для моніторингу переказів USDT / USDC](https://blockvectra.com/en/use-cases/stablecoin-payments/).

* **Перший крок:** [Створіть підписку та додайте одержувача до списку відстеження](#create-a-subscription-and-watch-the-recipient), почавши з API key та вашого HTTPS-обробника.
* **Критерій завершення:** відповідний переказ проходить перевірку підпису, мережі, токена, одержувача та цілочисельної суми, зберігається один раз як кандидат на платіж, а обробник повертає HTTP `204`; верифікуйте його ончейн відповідно до вашої політики підтверджень перед зарахуванням.

[Робочі процеси платежів у стейблкоїнах](https://blockvectra.com/en/use-cases/stablecoin-payments/).

Базовий моніторинг переказів уже доступний. Фільтрація сум і токенів виконується у вашому обробнику. Серверні умови, кілька стадій підтвердження та сповіщення в месенджери з'являться незабаром.

Для розробників та AI-агентів: почніть з [API key](https://console.blockvectra.com/login/?next=%2Fkeys%2F) і власного HTTPS-обробника; фільтруйте контракти токенів і суми у вашому застосунку. [Скопіюйте налаштування webhook](#create-a-subscription-and-watch-the-recipient).

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

* [Отримувати сповіщення про платежі USDT / USDC](#receive-payments-with-webhooks) на ваш HTTPS-ендпоінт після перевірки підтримки Push для обраної мережі.
* [Валідувати кандидата на переказ](#verify-deduplicate-and-validate-payments) шляхом перевірки мережі, контракту токена, одержувача та цілочисельної суми перед застосуванням ончейн-верифікації та політики підтверджень.
* [Виконувати бекфіл пропущених логів переказів](#cursor-polling-and-block-range-limits) за допомогою обмежених запитів `eth_getLogs` та збереженого курсора.

## Вибір між Webhook, WebSocket або опитуванням

| Метод                                            | Для чого використовувати                                                                   | Відновлення                                                                                                                       |
| ------------------------------------------------ | ------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------- |
| [Webhook](https://docs.blockvectra.com/en/guides/webhook-push/)              | Активність адреси, надіслана на ваш HTTPS-обробник, включно із вхідними переказами токенів | Верифікація підписів, дедуплікація ID подій та обробка `subscription.gap` / `chain.reorg`; повторне відтворення збережених збігів |
| [WebSocket](https://docs.blockvectra.com/en/guides/websocket-subscriptions/) | Відфільтровані `logs` через постійне з'єднання                                             | Повторне підключення, повторна підписка та бекфіл пропущених блоків                                                               |
| HTTP-опитування                                  | Моніторинг за розкладом або бекфіл історичних логів із власним курсором                    | Запит обмежених діапазонів `eth_getLogs` та збереження прогресу                                                                   |

Перед вибором WebSocket перевірте поля `ws` та `subscriptions` у публічній відповіді `GET /v1/chains`. Підтримка Push — це окрема перевірка: виконайте `GET /v1/push/chains` із вашим API key. Мережа без WebSocket може використовувати Webhook для адрес, якщо вона присутня в цьому списку. Використовуйте опитування, коли потрібно сканувати раніші блоки або працювати без постійного з'єднання.

## Отримання платежів за допомогою Webhook

### Створення підписки та відстеження одержувача

[Отримайте API key](https://blockvectra.com/en/get-api-key/) та розгорніть HTTPS-обробник на порту 443. Оберіть `CHAIN` зі списку автентифікованих Push-мереж, встановіть `RECIPIENT` як вашу депозитну адресу, а `RECEIVER_URL` — як URL вашого обробника. Цей приклад для shell вимагає `jq`; `{}` використовує стандартну кількість підтверджень мережі. Перевірте `min_confirmations`, `default_confirmations` та `max_confirmations` перед вибором іншої кількості. Специфікація [Push OpenAPI](https://docs.blockvectra.com/openapi/push.yaml) визначає ці запити.

```bash
set -eu
umask 077
: "${BLOCKVECTRA_API_KEY:?Set your API key}"
: "${CHAIN:?Select a chain from the Push chain list}"
: "${RECIPIENT:?Set the watched EVM recipient address}"
: "${RECEIVER_URL:?Set your HTTPS receiver URL}"
PUSH_URL='https://api.blockvectra.com/v1/push'

curl --fail-with-body -sS "$PUSH_URL/chains" \
  -H "x-api-key: $BLOCKVECTRA_API_KEY" > push-chains.json
jq -e --arg chain "$CHAIN" 'any(.chains[]; .chain == $chain)' push-chains.json
jq -n --arg url "$RECEIVER_URL" --arg chain "$CHAIN" \
  '{url: $url, chains: {($chain): {}}}' > create.json
curl --fail-with-body -sS "$PUSH_URL/subscriptions" \
  -H "x-api-key: $BLOCKVECTRA_API_KEY" \
  -H 'Content-Type: application/json' -d @create.json > subscription.json

SUBSCRIPTION_ID=$(jq -er '.id' subscription.json)
jq -n --arg recipient "$RECIPIENT" '{addresses: [$recipient]}' > addresses.json
curl --fail-with-body -sS "$PUSH_URL/subscriptions/$SUBSCRIPTION_ID/addresses/add" \
  -H "x-api-key: $BLOCKVECTRA_API_KEY" \
  -H 'Content-Type: application/json' -d @addresses.json > address-change.json
curl --fail-with-body -sS "$PUSH_URL/subscriptions/$SUBSCRIPTION_ID" \
  -H "x-api-key: $BLOCKVECTRA_API_KEY"
```

Створення повертає `id` та `secret`. Надійно збережіть secret для обробника; файл `subscription.json` містить облікові дані. Опитуйте підписку, доки `applied_version >= change_version` з `address-change.json`, після чого зафіксуйте `chains[CHAIN].applied_from_block`. Нові адреси зіставляються, починаючи з цього блоку, тому продовжуйте опитування для будь-якого ранішого інтервалу платежів.

### Верифікація, дедуплікація та валідація платежів

Збережіть [функцію перевірки підпису вихідного тіла запиту](https://docs.blockvectra.com/en/guides/webhook-push/#verify-signatures) як `verify-push.js`. Наведений нижче обробник приймає Web API `Request` у Node.js та читає його оригінальні байти перед парсингом JSON. Створіть `secrets` як `Map`, де рядки ID підписок відповідають збереженим секретам. Встановіть довірену конфігурацію `expected` у вигляді `{ chain, token, recipient, amountUnits }`: `token` — це верифікований контракт стейблкоїна в цій мережі, а `amountUnits` — очікувана додатна цілочисельна сума в її найменших неподільних одиницях. Порівнюйте суми за допомогою `BigInt`, ніколи не використовуючи числа з рухомою комою або символ токена.

```js
import { verifyPush } from './verify-push.js';

export function selectPayment(data, event, expected) {
  if (data.chain !== expected.chain || event.type !== 'token.transfer' ||
      event.standard !== 'erc20') return null;
  const address = value => typeof value === 'string' && /^0x[0-9a-fA-F]{40}$/.test(value);
  if (![event.token, event.to, expected.token, expected.recipient].every(address)) return null;
  if (event.token.toLowerCase() !== expected.token.toLowerCase() ||
      event.to.toLowerCase() !== expected.recipient.toLowerCase()) return null;
  const integer = value => typeof value === 'string' && /^[1-9][0-9]{0,77}$/.test(value);
  if (!integer(event.amount) || !integer(expected.amountUnits)) return null;
  const amount = BigInt(event.amount);
  if (amount >= (1n << 256n) || amount !== BigInt(expected.amountUnits)) return null;
  if (typeof event.id !== 'string' || typeof event.ref !== 'string' ||
      !/^0x[0-9a-f]{64}$/.test(event.tx_hash) ||
      !/^0x[0-9a-f]{64}$/.test(event.block_hash) ||
      !Number.isSafeInteger(event.log_index) || event.log_index < 0 ||
      !Number.isSafeInteger(event.block_number) || event.block_number < 0) return null;
  return {
    eventId: event.id, ref: event.ref, chain: data.chain,
    token: event.token, recipient: event.to, amountUnits: event.amount,
    txHash: event.tx_hash, logIndex: event.log_index,
    blockHash: event.block_hash, blockNumber: event.block_number,
  };
}

export async function receivePayments(request, expected, secrets, store) {
  const rawBody = Buffer.from(await request.arrayBuffer());
  const headers = Object.fromEntries(request.headers);
  if (!verifyPush(rawBody, headers, secrets)) return new Response(null, { status: 401 });
  let message;
  try { message = JSON.parse(rawBody.toString('utf8')); }
  catch { return new Response(null, { status: 400 }); }
  const data = message?.data;
  if (message?.type !== 'push.events' ||
      !Number.isSafeInteger(data?.subscription_id) || data.subscription_id <= 0 ||
      String(data.subscription_id) !== headers['bv-subscription-id'] ||
      data.chain !== expected.chain || !Array.isArray(data.events)) {
    return new Response(null, { status: 400 });
  }
  try {
    await store.transaction(async tx => {
      for (const event of data.events) {
        if (!event || typeof event.id !== 'string') continue;
        const recovery = event.type === 'subscription.gap' || event.type === 'chain.reorg';
        const payment = selectPayment(data, event, expected);
        if (!recovery && !payment) continue;
        if (!await tx.insertEventOnce(data.subscription_id, event)) continue;
        if (recovery) await tx.enqueueRecovery(data.chain, event);
        else await tx.recordPaymentCandidate(payment);
      }
    });
  } catch {
    return new Response(null, { status: 503 });
  }
  return new Response(null, { status: 204 });
}
```

Реалізуйте `store.transaction` за допомогою надійного сховища. У межах однієї транзакції `insertEventOnce` вставляє подію за унікальним ключем `(subscription_id, event.id)` і повертає false у разі дубліката; фіксуйте її разом із `recordPaymentCandidate` або `enqueueRecovery`. Відкочуйте всі операції запису в разі збою, щоб повторна спроба могла обробити подію. Завдання відновлення також мають бути ідемпотентними. Повертайте 2xx упродовж 10 секунд лише після фіксації транзакції; забезпечте ліміт розміру тіла запиту в 1 MiB на вашому HTTP-сервері.

Цей приклад перевіряє одну очікувану суму платежу. Для кількох замовлень знаходьте довірену конфігурацію платежу за мережею, токеном і одержувачем та узгоджуйте часткові чи надлишкові платежі за власними правилами. Кандидат усе ще потребує ончейн-верифікації та застосування вашої політики підтверджень перед зарахуванням коштів. Між підписками та опитуванням звіряйте той самий переказ за мережею, хешем транзакції та індексом логу, щоб два шляхи доставки не зарахували його двічі; зберігайте хеш блоку для відстеження замінених блоків.

### Відновлення пропущених або замінених блоків

Для `subscription.gap` додайте в чергу сканування блоків від `from_block` до `to_block`, використовуючи шлях опитування нижче або доступні набори даних Data API. `chain.reorg` — це безкоштовне сповіщення про те, що доставлені блоки були замінені, а не пробіл у доставці. Позначте або відкиньте старі події в цьому діапазоні за полем `ref`; узгодьте записи платежів за `ref` та `tx_hash` із канонічним ланцюгом перед обробкою автоматично повторно доставлених канонічних подій із новими ID. Дедуплікуйте ці події за `id`. Сповіщення про реорганізацію не просуває завершений прогрес; фіксуйте `complete_through_block` для кожної мережі, ніколи не роблячи висновків про завершення за найбільшим номером блоку події.

[Replay](https://docs.blockvectra.com/en/guides/webhook-push/#delivery-retries-and-replay) приймає `chain` та `from_block` у межах поточної межі `replayable_from_block`. Він лише повторно надсилає збережені збіги; він не сканує періоди до додавання адреси чи мережі або періоди, коли підписка була офлайн. Зберігайте курсор опитування для покриття цих інтервалів та застарілих прогалин. Збої запитів і недійсні діапазони повторного відтворення розглядаються в [довіднику помилок](https://docs.blockvectra.com/en/errors/); нарахування за доставку, історію та дні адрес описані в [правилах білінгу](https://docs.blockvectra.com/en/guides/billing-rules/#webhook-push-billing).

У наступних розділах реалізовано фільтрацію логів ERC-20 та опитування на основі курсора для моніторингу та відновлення.

## Подія Transfer та параметри фільтра

Стандартні контракти токенів ERC-20 генерують таку подію під час кожного переказу:

```solidity
event Transfer(address indexed from, address indexed to, uint256 value);
```

Під час виклику `eth_getLogs` передайте адресу контракту токена та масив `topics` для фільтрації відповідних логів:

| Параметр    | Значення                                                             | Опис                                                                                                                                                                                                                                                                 |
| ----------- | -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `address`   | Адреса контракту токена (або масив адрес)                            | Цільова адреса контракту стейблкоїна. Можна вказати одну адресу (наприклад, BSC USDT `0x55d398326f99059fF775485246999027B3197955`, Base USDC `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`) або масив адрес для одночасного моніторингу кількох токенів               |
| `topics[0]` | `0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef` | Хеш сигнатури події: `keccak256("Transfer(address,address,uint256)")`                                                                                                                                                                                                |
| `topics[1]` | `null`                                                               | Адреса відправника (`from`). Оскільки моніторинг депозитів приймає кошти з будь-якого гаманця користувача, передайте `null`, щоб відповідати будь-якому відправнику                                                                                                  |
| `topics[2]` | 32-байтна адреса одержувача, доповнена нулями                        | Адреса призначення (`to`). Згідно зі специфікаціями логів EVM, `indexed` параметри адрес займають 32 байти (64 шістнадцяткові символи). Доповніть 20-байтну адресу одержувача зліва 12 нульовими байтами (24 шістнадцяткові нулі) для формування 32-байтного топіка. |
| `fromBlock` | Початковий блок (шістнадцятковий)                                    | Початок діапазону блоків запиту (включно)                                                                                                                                                                                                                            |
| `toBlock`   | Кінцевий блок (шістнадцятковий)                                      | Кінець діапазону блоків запиту (включно)                                                                                                                                                                                                                             |

Неіндексоване значення `value` (сума переказу) закодовано в полі `data` об'єкта логу як 32-байтний шістнадцятковий `uint256`. Розділіть цю необроблену суму на 10^decimals, щоб отримати зрозумілу для людини суму токенів (наприклад, 18 десяткових знаків для BSC USDT; 6 десяткових знаків для Base та Ethereum USDC).

## Опитування за курсором та обмеження діапазону блоків

Сервіс опитування запитує нові блоки через регулярні проміжки часу (наприклад, кожні 3–5 секунд).

### Просування курсора

Підтримуйте у своїй базі даних постійний курсор `last_polled_block` (найвищий оброблений та зафіксований блок):

1. Для кожного циклу опитування встановіть `fromBlock = last_polled_block + 1`.
2. Запитайте поточну вершину ланцюга через `eth_blockNumber` та розрахуйте безпечну цільову висоту `safe_head` на основі вашої глибини підтвердження.
3. Якщо `fromBlock <= safe_head`, запитуйте логи частинами аж до `safe_head`. Після успішної обробки кожного фрагмента просувайте курсор.

### Обмеження діапазону блоків

Діапазон блоків одного виклику `eth_getLogs` розраховується як `toBlock − fromBlock + 1`. Він не повинен перевищувати `max_logs_block_range`, опублікований для цієї мережі в `GET /v1/chains`.

Якщо запит перевищує цей діапазон, сервіс відхиляє виклик із кодом помилки `-32602`:

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32602,
    "message": "eth_getLogs block range too large: max 1000 blocks",
    "data": {
      "reason": "logs_range_too_large",
      "docs_url": "https://docs.blockvectra.com/en/errors/#logs_range_too_large",
      "retryable": false
    }
  }
}
```

Запити, що перевищують діапазон блоків, повертають помилку JSON-RPC `-32602` (не тарифікується). У логіці вашого застосунку прочитайте `max_logs_block_range` із `GET /v1/chains` та обмежуйте кожен фрагмент опитування: `chunk_end = min(fromBlock + max_logs_block_range - 1, safe_head)`.

## Обробка реорганізацій блоків та глибина підтвердження

Біля вершини блокчейну можуть відбуватися тимчасові реорганізації блоків (reorgs). Зарахування платежів за станом `latest` без урахування глибини підтвердження несе ризик зарахування транзакцій з ізольованої гілки, яка згодом відкидається.

Застосовуйте такі заходи захисту для обробки платежів:

### Глибина підтвердження

Замість виконання запитів аж до `latest`, запитуйте дані до безпечної цільової висоти блоку:

`safe_head = current_head - CONFIRMATION_DEPTH`

Встановіть `CONFIRMATION_DEPTH` відповідно до допустимого рівня ризику вашого застосунку. Запит лише до `safe_head` гарантує, що оброблятимуться тільки блоки з достатньою кількістю підтверджень.

### Реорганізації під час опитування

Стандартний EVM JSON-RPC встановлює `removed: true` для об'єктів логів лише у потоках підписок на логи через WebSocket, коли раніше згенерована подія скасовується через реорганізацію ланцюга. Під час опитування через HTTP за допомогою `eth_getLogs` запити повертають логи з канонічного ланцюга; логи з відкинутих гілок просто не з'являться в наступних запитах. Опитування в межах `safe_head` гарантує, що платежі оброблятимуться лише на блоках із достатньою кількістю підтверджень.

## Дедуплікація за (transactionHash, logIndex)

Слухачі платежів повинні забезпечувати сувору ідемпотентність:

1. **Кілька переказів в одній транзакції**: одна транзакція може містити декілька подій `Transfer` на ту саму депозитну адресу (наприклад, маршрутизатори токенів, що розбивають обміни, або контракти масових виплат). **Важливо:** `transactionHash` сам по собі не є унікальним для кожного платежу.
2. **Перекриття опитування та повторні спроби**: коли сервіси опитування перезапускаються, відновлюються після тимчасових мережевих помилок або повертаються на кілька блоків назад для обробки неглибоких реорганізацій, логи з того самого діапазону блоків запитуються кілька разів.
3. **Унікальність індексу логу**: `logIndex` визначає відносну позицію логу події в межах блоку. За специфікаціями EVM канонічним складеним унікальним ідентифікатором події є `(transactionHash, logIndex)`.

У схемах реляційних баз даних оголосіть складений унікальний індекс для таблиці записів депозитів:

```sql
CREATE UNIQUE INDEX idx_transfers_tx_log ON deposit_records (transaction_hash, log_index);
```

Перед обробкою депозиту виконуйте перевірку за наявними записами `(transactionHash, logIndex)`, щоб гарантувати зарахування кожного ончейн-переказу рівно один раз.

## Повні приклади коду

У наведених нижче прикладах показано отримання можливостей мережі з `/v1/chains`, розрахунок безпечних діапазонів блоків, опитування логів `Transfer` стейблкоїнів з урахуванням обмежень діапазону та дедуплікацію подій.

**TypeScript**

```ts
import { createPublicClient, formatUnits, http, parseAbiItem } from "viem";

const apiKey = process.env.BLOCKVECTRA_API_KEY;
if (!apiKey) {
  throw new Error("BLOCKVECTRA_API_KEY environment variable is not set");
}

const CHAIN = "bsc_mainnet";
const RPC_URL = "https://api.blockvectra.com/v1/bsc_mainnet";
const CHAINS_URL = "https://api.blockvectra.com/v1/chains";

// Цільова адреса контракту стейблкоїна (у цьому прикладі використовується BSC USDT)
const TOKEN_CONTRACT = "0x55d398326f99059fF775485246999027B3197955" as const;
const TOKEN_DECIMALS = 18;

// Відстежувана депозитна адреса
const RECIPIENT_ADDRESS = "0xdded13D555B6DA811103cC1794D3d4330F69632C" as const;

// Глибина підтвердження для захисту від реорганізацій ланцюга
const CONFIRMATION_DEPTH = 15n;

// 1. Отримайте можливості мережі з публічного ендпоінта метаданих (без автентифікації, не тарифікується)
const chainsRes = await fetch(CHAINS_URL);
const { chains } = (await chainsRes.json()) as {
  chains: Array<{
    chain: string;
    ws: boolean;
    subscriptions: string[];
    max_logs_block_range: number;
  }>;
};

const chainConfig = chains.find((c) => c.chain === CHAIN);
if (!chainConfig) {
  throw new Error(`Chain ${CHAIN} not found in /v1/chains`);
}

const maxLogsRange = BigInt(chainConfig.max_logs_block_range || 1000);
console.log(`Chain: ${CHAIN} | WebSocket supported: ${chainConfig.ws} | Max logs range: ${maxLogsRange}`);

// 2. Ініціалізуйте клієнт viem із заголовком x-api-key
const client = createPublicClient({
  transport: http(RPC_URL, {
    fetchOptions: {
      headers: { "x-api-key": apiKey },
    },
  }),
});

// Set для відстеження оброблених подій за складеним ключем: (transactionHash, logIndex)
const processedLogs = new Set<string>();

// 3. Розрахуйте діапазон запиту: відніміть глибину підтвердження від поточної вершини
const currentHead = await client.getBlockNumber();
const safeHead = currentHead - CONFIRMATION_DEPTH;

// Для демонстрації почніть курсор за 10 блоків до safeHead
let cursor = safeHead > 10n ? safeHead - 10n : 0n;

console.log(`Current head: ${currentHead} | Safe head: ${safeHead} | Polling cursor: ${cursor}`);

while (cursor <= safeHead) {
  const chunkEnd = cursor + maxLogsRange - 1n < safeHead ? cursor + maxLogsRange - 1n : safeHead;

  const logs = await client.getLogs({
    address: TOKEN_CONTRACT,
    event: parseAbiItem(
      "event Transfer(address indexed from, address indexed to, uint256 value)"
    ),
    args: {
      to: RECIPIENT_ADDRESS,
    },
    fromBlock: cursor,
    toBlock: chunkEnd,
  });

  for (const log of logs) {
    const dedupKey = `${log.transactionHash}-${log.logIndex}`;
    if (processedLogs.has(dedupKey)) {
      continue;
    }
    processedLogs.add(dedupKey);

    const tokenAmount = formatUnits(log.args.value ?? 0n, TOKEN_DECIMALS);

    console.log(
      `[Payment Received] Amount: ${tokenAmount} | ` +
      `Tx: ${log.transactionHash} | Log: ${log.logIndex} | Block: ${log.blockNumber}`
    );
  }

  cursor = chunkEnd + 1n;
}

// Запуск: npx tsx example.mts
```


  **Python**

```python
from decimal import Decimal
import os
import requests

api_key = os.environ.get("BLOCKVECTRA_API_KEY")
if not api_key:
    raise RuntimeError("BLOCKVECTRA_API_KEY environment variable is not set")

CHAIN = "bsc_mainnet"
RPC_URL = "https://api.blockvectra.com/v1/bsc_mainnet"
CHAINS_URL = "https://api.blockvectra.com/v1/chains"

# Цільова адреса контракту стейблкоїна (у цьому прикладі використовується BSC USDT)
TOKEN_CONTRACT = "0x55d398326f99059fF775485246999027B3197955"
TOKEN_DECIMALS = 18

# Відстежувана депозитна адреса
RECIPIENT_ADDRESS = "0xdded13D555B6DA811103cC1794D3d4330F69632C"

# Хеш сигнатури Transfer(address,address,uint256)
TRANSFER_TOPIC0 = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"

# Доповніть 20-байтну адресу зліва до 32 байтів (64 шістнадцяткові символи)
padded_recipient = f"0x{RECIPIENT_ADDRESS.lower()[2:].rjust(64, '0')}"

# Глибина підтвердження для захисту від реорганізацій ланцюга
CONFIRMATION_DEPTH = 15

# 1. Отримайте можливості мережі з публічного ендпоінта метаданих (без автентифікації, не тарифікується)
chains_res = requests.get(CHAINS_URL, timeout=10)
chains_res.raise_for_status()
chain_list = chains_res.json().get("chains", [])

chain_config = next((c for c in chain_list if c["chain"] == CHAIN), None)
if not chain_config:
    raise RuntimeError(f"Chain {CHAIN} not found in /v1/chains")

max_logs_range = chain_config.get("max_logs_block_range", 1000)
ws_supported = chain_config.get("ws", False)
print(f"Chain: ${CHAIN} | WebSocket supported: {ws_supported} | Max logs range: {max_logs_range}")

def rpc_request(method: str, params: list):
    res = requests.post(
        RPC_URL,
        headers={
            "Content-Type": "application/json",
            "x-api-key": api_key,
        },
        json={"jsonrpc": "2.0", "id": 1, "method": method, "params": params},
        timeout=15,
    )
    res.raise_for_status()
    payload = res.json()
    if "error" in payload:
        err = payload["error"]
        raise RuntimeError(f"JSON-RPC error {err.get('code')}: {err.get('message')}")
    return payload["result"]

# 2. Запитайте номер останнього блоку та розрахуйте safe head
current_head_hex = rpc_request("eth_blockNumber", [])
current_head = int(current_head_hex, 16)
safe_head = max(0, current_head - CONFIRMATION_DEPTH)

# Для демонстрації почніть курсор за 10 блоків до safe_head
cursor = max(0, safe_head - 10)
print(f"Current head: {current_head} | Safe head: {safe_head} | Polling cursor: {cursor}")

# Множина в пам'яті для дедуплікації за допомогою (transactionHash, logIndex)
processed_logs = set()

while cursor <= safe_head:
    chunk_end = min(cursor + max_logs_range - 1, safe_head)

    logs = rpc_request(
        "eth_getLogs",
        [
            {
                "address": TOKEN_CONTRACT,
                "fromBlock": hex(cursor),
                "toBlock": hex(chunk_end),
                "topics": [
                    TRANSFER_TOPIC0,
                    None,  # відповідає будь-якому відправнику
                    padded_recipient,  # відповідає відстежуваному одержувачу
                ],
            }
        ],
    )

    for log in logs:
        tx_hash = log["transactionHash"]
        log_index = int(log["logIndex"], 16)
        dedup_key = (tx_hash, log_index)

        if dedup_key in processed_logs:
            continue
        processed_logs.add(dedup_key)

        raw_amount = int(log["data"], 16)
        token_amount = Decimal(raw_amount) / (Decimal(10) ** TOKEN_DECIMALS)
        block_number = int(log["blockNumber"], 16)

        print(
            f"[Payment Received] Amount: {token_amount} | "
            f"Tx: {tx_hash} | Log: {log_index} | Block: {block_number}"
        )

    cursor = chunk_end + 1

# Запуск: python example.py
```


## Правила білінгу та пов'язані посібники

* Докладну інформацію про облік запитів, ваги в CU та правила тарифікації кодів помилок дивіться у розділі [Правила білінгу: помилки та незатарифіковані запити](https://docs.blockvectra.com/en/guides/billing-rules/).
* Поглиблений посібник щодо обмежень діапазону блоків `eth_getLogs` та логіки розбиття на фрагменти дивіться у статті [Обмеження діапазону блоків eth\_getLogs та фрагментовані запити](https://docs.blockvectra.com/en/guides/getlogs-block-range/).
* Про відмінності між запитами до RPC-вузла в реальному часі та API індексованої історії переказів читайте у посібнику [Вершина ланцюга проти індексованої історії: коли використовувати eth\_getLogs, а коли Transfers](https://docs.blockvectra.com/en/guides/logs-vs-transfers/).

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

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