getProgramAccounts повертає всі акаунти, що належать заданій програмі. Без фільтрів цей виклик змушує RPC-вузол повністю сканувати AccountsDB за програмним ID — операція, яка в production може тривати секунди, повертати мегабайти даних і призвести до rate-limiting або блокування з боку провайдера. Нижче описано, як звузити запит до мінімально необхідного обсягу, контролювати розмір відповіді та коли взагалі відмовитися від цього методу на користь індексування.

Передумови: робота з Solana RPC через @solana/web3.js (перевірте актуальну версію у репозиторії проєкту), розуміння байтової структури акаунтів вашої програми (розмір у байтах, зміщення полів), доступ до RPC-вузла, який не блокує getProgramAccounts (деякі публічні провайдери обмежують цей метод).

Очікуваний результат: зниження розміру JSON-відповіді на порядки, стабільний час відповіді (менше 500 мс для типового запиту з фільтрами), відсутність rate-limiting.

Ризики: некоректне значення offset у memcmp призводить до хибних результатів без помилки від RPC; зміна структури акаунта після оновлення програми ламає фільтри; надмірна кількість фільтрів може відхилятися провайдером.

Спосіб перевірки: порівняйте розмір відповіді та час виконання запиту з фільтрами і без них на тестовому RPC-вузлі за допомогою curl або логування в застосунку.

Увага: наведені нижче приклади коду призначені для ілюстрації логіки фільтрів. Перед використанням у production перевірте їх на тестовому середовищі з актуальною версією бібліотеки.

Фільтрація за dataSize та memcmp

Фільтри передаються в параметрі config.filters як масив об'єктів. Усі фільтри комбінуються за логікою AND — акаунт має відповідати кожному з них, інакше він виключається з результату.

Фільтр dataSize

Приймає число — точний розмір даних акаунта в байтах. Цей розмір не включає 165 байт заголовка акаунта (header), а лише серіалізовані дані, які записані програмою. Фільтр корисний, коли програма містить кілька типів акаунтів різного розміру.

Приклад: якщо UserAccount займає 128 байтів, а ConfigAccount — 64 байти, фільтр dataSize: 128 поверне лише екземпляри UserAccount.

filters: [
  { dataSize: 128 }
]

Типова помилка: вказати розмір серіалізованої структури з Borsh, забувши додати 8 байтів Anchor-дискримінатора, якщо програма написана на Anchor. Наслідок — фільтр не збігається з реальним розміром, і RPC повертає порожній масив без повідомлення про помилку. Перевірте реальний розмір через getAccountInfo для відомого акаунта вашої програми.

Фільтр memcmp

Порівнює фрагмент даних акаунта за заданим зміщенням з очікуваним значенням. Параметри:

  • offset — зміщення в байтах від початку даних акаунта (не від заголовка)
  • bytes — значення для порівняння, за замовчуванням у кодуванні base58
  • encoding"base58" (за замовчуванням) або "base64"

Найпоширеніший випадок — фільтрація за Anchor-дискримінатором (перші 8 байтів даних акаунта). Дискримінатор обчислюється як SHA-256("global:<account_name>")[0..8] і кодується в base58. Це дозволяє відфільтрувати акаунти конкретного типу навіть якщо кілька типів мають однаковий dataSize.

Інший поширений випадок — фільтрація за полем типу Pubkey (32 байти) за відомим зміщенням, наприклад, за полем authority або mint.

filters: [
  {
    memcmp: {
      offset: 0,
      bytes: "Base58EncodedDiscriminator",
      encoding: "base58"
    }
  }
]

Типова помилка: передати bytes у форматі base64 без явного вказання encoding: "base64". RPC інтерпретуватиме рядок як base58, порівняння не збіжиться, і ви отримаєте порожній масив результатів без жодного повідомлення про помилку. Це одна з найскладніших для діагностики проблем, оскільки з боку RPC запит виконано коректно.

Комбінування фільтрів та обмеження

Масив фільтрів обробляється послідовно, кожен звужує множину результатів. Практичний приклад — отримати всі UserAccount конкретного користувача в програмі на Anchor:

filters: [
  { dataSize: 128 },
  {
    memcmp: {
      offset: 8, // після 8 байтів дискримінатора
      bytes: "Base58EncodedAuthorityPubkey",
      encoding: "base58"
    }
  }
]

Обмеження: максимальна кількість фільтрів у одному запиті залежить від RPC-провайдера. Для стандартних вузлів Solana це зазвичай до 4 фільтрів. Деякі комерційні провайдери підтримують більше, але це не стандартизовано. Перевірте документацію вашого провайдера перед розгортанням.

Пагінація результатів

getProgramAccounts не має вбудованої курсорної пагінації на кшталт getSignaturesForAddress. Ви не можете запросити "сторінку 2" або вказати зміщення в масиві результатів. Це архітектурне обмеження методу, а не брак функціоналу в бібліотеці.

Є два робочі підходи, кожен із компромісами.

Перший — параметр dataSlice. Він дозволяє отримати лише фрагмент даних акаунта замість повного обсягу:

dataSlice: {
  offset: 0,
  length: 0 // поверне лише pubkey акаунта без даних
}

Це корисно для двоетапного запиту: спочатку отримати список pubkey із dataSlice, потім завантажити повні дані потрібних акаунтів через getMultipleAccounts. Підхід зменшує обсяг першої відповіді, але подвоює кількість RPC-викликів. Крім того, getMultipleAccounts має власне обмеження на кількість pubkey за один виклик (зазвичай до 100).

Другий — додаткова фільтрація за значенням поля. Якщо акаунти мають поле, яке можна використати як порядок (наприклад, лічильник або timestamp), можна виконати кілька запитів із різними значеннями memcmp для цього поля, імітуючи сторінки. Це працює лише за наявності відповідного поля, знання діапазону значень і значної інженерної роботи на стороні клієнта.

Обидва підходи є workaround, а не справжньою пагінацією. Якщо ваш сервіс потребує надійного поділу результатів на сторінки, getProgramAccounts — не той інструмент.

Вплив на навантаження RPC-вузла

Коли ви викликаєте getProgramAccounts, RPC-вузол виконує повний скан AccountsDB за програмним ID. Фільтри dataSize та memcmp зменшують обсяг даних, що передається по мережі, але не обов'язково зменшують обсяг роботи вузла — сканування та перевірка кожного акаунта все одно відбуваються.

Що відбувається на рівні вузла:

  1. Пошук усіх акаунтів із owner == programId у AccountsDB
  2. Застосування фільтрів до кожного знайденого акаунта (читання даних, порівняння)
  3. Серіалізація результатів у JSON
  4. Відправка відповіді клієнту

Для програм із тисячами акаунтів кроки 1–2 можуть зайняти сотні мілісекунд навіть із фільтрами. Для програм із десятками тисяч акаунтів — секунди. Серіалізація великого масиву (крок 3) також споживає CPU та пам'ять вузла.

Публічні RPC-вузли Solana обмежують або блокують виклики getProgramAccounts без фільтрів. Деякі провайдери встановлюють жорсткі ліміти на розмір відповіді (наприклад, 10 МБ) і повертають помилку при перевищенні. Інші обмежують частоту викликів цього методу окремо від загальних rate-limitів.

Як мінімізувати вплив:

  • Завжди вказуйте хоча б один фільтр — навіть dataSize alone значно зменшує обсяг відповіді
  • Використовуйте dataSlice, якщо вам не потрібні повні дані акаунтів на цьому етапі
  • Кешуйте результати на стороні застосунку, якщо дані не потребують реального часу
  • Уникайте частих викликів у циклах — агрегуйте запити де це можливо
  • Не викликайте getProgramAccounts у відповідь на кожну транзакцію користувача; використовуйте події або WebSocket-підписки для інкрементальних оновлень

Метрики для моніторингу: час відповіді (response time), розмір тіла відповіді (response body size у байтах), кількість помилок 429. Фіксуйте їх у вашій системі моніторингу з розбивкою за типом запиту, щоб виявити деградацію до того, як вона стане критичною для користувачів.

Альтернативи для великих наборів даних

Якщо програма має понад тисячу акаунтів або вам потрібні складні запити (фільтрація за кількома полями, сортування, агрегація, join), getProgramAccounts перестає бути придатним інструментом. Ось альтернативи з їхніми компромісами.

Підхід Переваги Обмеження Коли обрати
Geyser Plugin + власний індексатор Повний контроль над даними та запитами; миттєве оновлення через стріми Geyser; немає залежності від сторонніх API Висока інфраструктурна складність: потрібен окремий сервер, база даних, логіка індексації та її обслуговування Великі проєкти з інфраструктурною командою; вимоги до повної власності над даними
Комерційні індексатори Готові API з фільтрацією, пагінацією, вебхуками; швидкий старт; немає інфраструктурного навантаження на команду Залежність від стороннього провайдера; витрати зростають з обсягом даних і кількістю запитів; менший контроль над затримками Стартапи та команди без інфраструктурного ресурсу; прототипування; швидкий вихід на ринок
On-chain індексація (PDA-карти) Запит getAccountInfo за PDA замість getProgramAccounts; детермінований PDA-адресування; працює в рамках стандартного RPC Додаткові витрати на транзакції для оновлення індексу; обмеження на розмір одного акаунта (10 МБ теоретично, але практично — до кількох кілобайтів через вартість алокації); складність оновлення індексу при видаленні записів Програми з помірною кількічтю записів (до кількох тисяч); коли індекс є частиною бізнес-логіки

Коли переходити: якщо ваш виклик getProgramAccounts стабільно повертає понад 100 акаунтів або час відповіді перевищує 500 мс — це сигнал почати міграцію на один із вказаних підходів. Чекати, поки проблема стане критичною, означає ризикувати простоєм сервісу під час міграції. Моделювання даних для індексатора розглядається в окремому матеріалі цього розділу.

Джерела