Надійний клієнт для Solana — це не просто обгортка над HTTP-запитами до RPC. Це шар, який ізолює вашу бізнес-логіку від тимчасових відмов мережі, обмежень rate limit провайдерів та деградації окремих нод. Нижче — покрокова інженерна інструкція з реалізації такого клієнта: від класифікації помилок до перевіреного плану тестування.
Обробка тимчасових відмов RPC
Перший крок — чітко розмежувати помилки, які можна виправити повтором, від тих, де повтор безглуздий. Solana RPC повертає помилки у різних форматах залежно від провайдера, тому клієнт має нормалізувати їх до єдиної внутрішньої класифікації.
Класифікація помилок RPC
Усі помилки варто поділити на три групи:
- Тимчасові (retryable) — HTTP 429 (Too Many Requests), HTTP 503 (Service Unavailable), HTTP 502 (Bad Gateway), таймаут з'єднання, помилка парсингу відповіді через обрізаний JSON. Причина: перевантаження ноди, тимчасові проблеми мережі між вашим сервером і провайдером.
- Постійні (non-retryable) — HTTP 400 (Bad Request) із валідним тілом помилки, помилка валідації транзакції (наприклад, InstructionFallbackNotFound), недостатній баланс для комісії, невірний блокхейн-стан. Причина: логічна помилка у запиті або транзакції.
- Амбівалентні — помилка після відправки транзакції, коли ви не знаєте, чи транзакція потрапила в блок. Причина: таймаут на етапі sendTransaction, коли нода прийняла транзакцію, але підтвердження не надійшло. Цей випадок потребує окремої обробки — перевірки статусу по сигнатурі, а не сліпого повтору.
Практична реалізація нормалізації
Клієнт має перехоплювати сирі відповіді та перетворювати їх на типізовані об'єкти. Наприклад, при використанні TypeScript варто визначити єдиний enum для категорій помилок і гарантувати, що кожен HTTP-статус і кожне поле error.message з відповіді Solana мапляться на нього.
Критично важливий нюанс: різні провайдери по-різному повідомляють про rate limit. Один повертає HTTP 429 із заголовком Retry-After, інший — HTTP 200 із тілом {"error":{"code":-32429,"message":"Too many requests"}}. Клієнт має обробляти обидва варіанти.
Очікуваний результат: кожен виклик RPC повертає або валідну відповідь, або типізовану помилку із чітко встановленим полем retryable: boolean. Бізнес-логіка більше не парсить рядки помилок.
Ризик: некоректна класифікація призведе або до безкінечних повторів постійної помилки, або до втрати транзакцій через передчасну відмову від повтору.
Перевірка: залогуйте кожну помилку разом із її класифікацією на staging-середовищі і вручну перевірте, що жодна тимчасова помилка не потрапила в категорію постійних.
Стратегії повторних спроб з backoff
Простий цикл із фіксованою затримкою між повторами — недостатній для production. Solana-мережа має пікові навантаження (наприклад, під час запуску популярних NFT-колекцій або аірдропів), коли одночасно деградують десятки нод. Ваш клієнт має адаптуватися.
Exponential backoff з jitter
Базова формула: delay = base_delay * 2^attempt + random_jitter. Jitter обов'язковий — без нього всі ваші інстанси, що отримали відмову одночасно, зроблять повторний запит одночасно, посиливши навантаження на ноду.
Рекомендовані параметри для Solana RPC:
- base_delay — 500 мс для read-операцій (getBalance, getAccountInfo), 1000 мс для write-операцій (sendTransaction).
- max_delay — 8000 мс. Затримки понад це свідчать про серйозну деградацію, яку краще обробити через fallback до іншого провайдера.
- max_attempts — 3 для read-операцій, 5 для sendTransaction (транзакція дорожча за повтор, ніж read-запит).
- jitter — рівномірний розподіл у діапазоні [0, base_delay * 0.5].
Circuit breaker для захисту від каскадних відмов
Якщо певний endpoint постійно повертає помилки, клієнт має тимчасово припинити запити до нього, навіть якщо backoff ще не вичерпано. Реалізуйте патерн circuit breaker:
- Відстежуйте частку невдалих запитів у ковзному вікні (наприклад, останні 20 запитів).
- Якщо частка перевищує 60% — перемикайте ланцюг у стан open.
- У стані open — одразу повертаєте помилку без фактичного запиту до RPC.
- Через cooldown_period (наприклад, 30 секунд) — перемикайте в стан half-open: пропускаєте один тестовий запит. Якщо успішний — повертаєте в closed.
Межа застосування: circuit breaker має працювати на рівні окремого RPC-endpoint, а не глобально. Інакше один проблемний провайдер заблокує всі запити.
Очікуваний результат: при деградації ноди клієнт не витрачає час на очікування таймаутів, а швидко перемикається на інший endpoint.
План відкату: якщо circuit breaker дає хибні спрацьовування (наприклад, через занадто мале вікно), збільште розмір вікна до 50 запитів і поріг до 70%. Це можна зробити через конфігурацію без перерозгортання.
Fallback між кількома провайдерами
Жоден одиночний RPC-провайдер не гарантує uptime на рівні 99.9% для вашого специфічного патерну навантаження. Production-клієнт має працювати з пулом провайдерів і автоматично перемикатися.
Стратегія вибору провайдера
Найпростіший підхід — round-robin — працює погано, бо не враховує реальну доступність. Рекомендована стратегія: weighted random з динамічними вагами.
- Кожен провайдер має початкову вагу (наприклад, 1.0).
- При успішному запиті — вага незмінна.
- При тимчасовій помилці — вага зменшується (наприклад, множиться на 0.5).
- При постійній помилці — вага незмінна (це не проблема провайдера).
- Періодично (кожні 60 секунд) — часткове відновлення ваги до базового рівня, щоб дати провайдеру шанс повернутися.
Запит обирає провайдера випадково з урахуванням ваг. Це гарантує, що здоровий провайдер отримує більшість запитів, але проблемний не виключається повністю.
Консистентність даних при fallback
Критична проблема: різні провайдери можуть бути синхронізовані з різними слотами. Якщо ви читаєте стан акаунта з провайдера A, а відправляєте транзакцію через провайдера B, транзакція може бути відхилена через застарілий стан.
Рішення для read-then-write сценаріїв:
- Перед відправкою транзакції отримайте latestBlockhash з того самого провайдера, через який будете відправляти.
- Якщо використовуєте simulateTransaction для перевірки — викликайте його на тому самому провайдері.
- Для чистих read-операцій (без подальшого write) — fallback між провайдерами без обмежень, але фіксуйте розбіжність слотів у логах для діагностики.
Конфігурація пулу провайдерів
Рекомендується мінімум 3 провайдери з різною інфраструктурною базою. Пул має конфігуруватися зовні (environment variables, config-файл), а не хардкодитися. Кожен запис у пулі містить: URL, тип (public/private/dedicated), базову вагу, таймаут з'єднання, таймаут відповіді.
Очікуваний результат: при відключенні одного провайдера клієнт продовжує роботу без переривів, а розподіл запитів адаптується за кілька секунд.
Ризик: якщо всі провайдери належать одному підприємству (наприклад, всі використовують одну інфраструктуру бекенду), fallback не дає реальної відмовостійкості. Перевіряйте інфраструктурну незалежність провайдерів.
Тестування стабільності клієнта
Клієнт, який не протестований на стійкість до відмов, — це клієнт, який зламається в найгірший момент. Звичайні unit-тести перевіряють щасливий шлях; тут потрібен окремий підхід.
Мокований RPC-сервер для контрольованих відмов
Реалізуйте локальний HTTP-сервер, який імітує поведінку Solana RPC з можливістю програмувати відповіді. Сервер має підтримувати сценарії:
- Повертає HTTP 429 після N-го запиту.
- Затримує відповідь на заданий час (імітація таймауту).
- Повертає валідну відповідь, але з застарілим blockhash.
- Повертає обрізаний JSON (імітація розірваного з'єднання).
- Повертає успішну відповідь на sendTransaction, але наступний getSignatureStatuses завжди повертає null (імітація амбівалентної помилки).
Тести проганяють ваш клієнт проти цього сервера з перевірками: кількість фактичних HTTP-запитів, загальний час виконання, кінцевий результат (успіх чи типізована помилка).
Інтеграційні тести на staging з реальними провайдерами
Мокований сервер не відтворює реальну поведінку мережі. Додатково потрібні тести на staging-середовищі з реальними RPC-endpointами, але з обережністю:
- Використовуйте devnet, а не mainnet, щоб уникнути витрат SOL.
- Не відправляйте масові транзакції — обмежтеся getAccountInfo, getBalance, getLatestBlockhash.
- Фіксуйте реальні часи відповідей і порівнюйте з очікуваними діапазонами.
Навантажувальні тести з моделюванням піків
Прогоніть клієнта під поступово зростаючим RPS (requests per second), починаючи з 10 і доходячи до вашого очікуваного пікового навантаження × 2. Фіксуйте:
- Чи зростає латентість лінійно чи експоненціально.
- Чи спрацьовує circuit breaker коректно (без хибних спрацьовувань при нормальному навантаженні).
- Чи розподіляються запити між провайдерами відповідно до ваг.
- Чи є витоки з'єднань (перевірте кількість відкритих socket-з'єднань після тесту).
Перевірка амбівалентних сценаріїв
Це найскладніший і найважливіший тест. Мокований сервер приймає sendTransaction, повертає сигнатуру, але на всі наступні getSignatureStatuses відповідає null. Перевірте, що клієнт:
- Не повторює sendTransaction з тією самою транзакцією (це призведе до дублювання, якщо перша транзакція все ж потрапила в блок).
- Повідомляє бізнес-логіку про невизначений статус, а не про помилку чи успіх.
- Зберігає сигнатуру для подальшого ручного або автоматичного перевірення.
Очікуваний результат: набір тестів, який можна запустити в CI/CD і який вловлює регресії стабільності клієнта при кожному зміні коду.
Межа: ці тести не замінюють повноцінне chaos-тестування інфраструктури (яке належить до іншого рівня). Вони перевіряють саме клієнтський шар ізольовано.
Наступний логічний крок: після реалізації та тестування надійного клієнта варто інтегрувати його з вашим інструментариєм діагностики. Якщо observability ще не налаштовано, зверніться до матеріалу про те, як налаштувати observability для Solana-бекенду. Якщо клієнт вже працює стабільно, але вам потрібно оновити on-chain програму — читайте про як відкочувати оновлення програми.