Запросы к узлу: fetch, JSON-RPC и таймауты
- Зачем это нужно
- Запрос состоит из четырёх частей
- Форма разговора с узлом
- Числа приходят строками — и обычное превращение их портит
- Отказ узла — не сетевая ошибка
- Подряд или вместе
- Без ограничения по времени скрипт висит
- Что здесь не ловит компилятор
- Типичные ошибки
- Слова главы
- Тренажёр: набрать вслепую
- На чемпионате
- Что дальше
- Углубиться
- Куда дальше: проверенные ресурсы
Зачем это нужно
Предыдущая глава дала обещания и ожидание результата на выдуманных задержках. Здесь та же машинерия применяется к единственному, ради чего она в этом треке и нужна: разговору с узлом блокчейна.
Узел — обычный сервер, отвечающий на запросы по обычному сетевому протоколу. Никакой магии: тот же способ, которым программа спрашивает погоду, спрашивает и номер последнего блока.
Разберём три вещи, каждую с прогоном: как устроен разговор, почему отказ узла — это не сетевая ошибка, и почему без ограничения по времени скрипт может висеть вечно.
Запрос состоит из четырёх частей
Метод — глагол: «отдай» или «на, обработай». Узлы принимают вызовы только вторым способом, потому что вызов надо передать телом.
Адрес — куда стучимся. У локальной сети это адрес на своей же машине с номером порта.
Заголовки — служебные пометки; главная говорит, что тело — это текст в формате обмена данными.
Тело — сам запрос: что именно спрашиваем.
const res = await fetch("http://127.0.0.1:8545", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_blockNumber", params: [] }),
});
const body = await res.json();
Форма разговора с узлом
Тело запроса всегда одинаковой формы — четыре поля:
| Поле | Что означает |
|---|---|
| версия | какой договорённости следуем |
| метод | что спрашиваем: eth_blockNumber, eth_getBalance, eth_chainId |
| параметры | список аргументов; часто пустой |
| номер | чтобы сопоставить ответ с запросом |
Ответ бывает ровно двух видов: с полем результата или с полем ошибки. Третьего не бывает, и это важно для следующего раздела.
Числа приходят строками — и обычное превращение их портит
Первая модель спрашивает у локального узла три вещи:
HTTP 200 application/json
сырой ответ : {"jsonrpc":"2.0","id":1,"result":"0x1263b4c"}
номер блока : 19282764 | через BigInt: 19282764n
HTTP 200 application/json
сырой ответ : {"jsonrpc":"2.0","id":2,"result":"0x1bc16d674ec80001"}
баланс, wei : 2000000000000000001n
баланс, ETH : 2.000000000000000001
через Number: 2000000000000000000 <- потерялся один wei
HTTP 200 application/json
сеть номер : 31337
Числа приезжают строками в шестнадцатеричном виде, и это не прихоть: обычное число в формате обмена данными не вмещает баланс в мельчайших единицах. Выше определённого порога арифметика начинает врать.
Третья строка блока показывает это буквально: баланс 2000000000000000001 после обычного превращения стал 2000000000000000000. Один wei исчез молча.
Правило: суммы и балансы превращают в большие целые числа, а не в обычные. Ту же осторожность мы уже видели в Solidity, где всё считалось в мельчайших единицах.
Отказ узла — не сетевая ошибка
Та же модель делает опечатку в имени метода:
HTTP 200 application/json
сырой ответ : {"jsonrpc":"2.0","id":4,"error":{"code":-32601,
"message":"the method eth_blockNumbr does not exist/is not available"}}
res.ok был true, сетевой ошибки не было — ошибка лежит внутри тела
код ошибки : -32601 | текст: the method eth_blockNumbr does not exist…
Это половина главы, поэтому разберём медленно. Отказов две породы.
Сеть молчит. Узел не отвечает, порт закрыт, адрес неверен. Запрос отклоняется с ошибкой, и её ловит обычный перехват. Настоящая причина при этом прячется в поле «причина» у ошибки — само сообщение бесполезно общее.
Узел ответил и сказал «нет». Статус 200, признак успеха истинен, никакого исключения нет — а в теле лежит поле ошибки. Перехват здесь не сработает: бросать нечего.
Отсюда правило, которое стоит запомнить дословно: после каждого запроса проверяют и статус ответа, и поле ошибки в теле. Пропустите второе — и опечатка в имени метода превратится в тихую пустоту, которая всплывёт через двадцать строк.
Заодно: неуспешный статус ответа — тоже не исключение. Запрос считается выполненным, если сервер вообще ответил, каким бы ни был статус.
Подряд или вместе
Вторая модель делает десять одинаковых запросов к серверу, который отвечает через сотню миллисекунд:
подряд : 1000 мс | пик одновременных на сервере: 1
вместе : 100 мс | пик одновременных на сервере: 10
выигрыш : 10 раз
порядок в Promise.all сохранён: результаты идут как в массиве, а не как пришли
Разница в десять раз — не новость. Новость в счётчике со стороны сервера: в первом случае к нему в каждый момент стучался один запрос, во втором — десять. Клиент не ускорился; он просто перестал ждать.
Отсюда и три случая, когда так делать не надо:
- результат нужен для следующего шага — сначала адрес, потом баланс по этому адресу: выбора нет;
- сервер не выдержит — публичные узлы ограничивают частоту и в ответ начнут отказывать;
- порядок важен для сети — транзакции с одного адреса нумеруются подряд, и отправлять их пачкой значит перепутать номера.
Для чтения метрик дашборда параллельность — то, что нужно. Для отправки транзакций — почти всегда нет.
Без ограничения по времени скрипт висит
Ограничения по времени нет по умолчанию вообще. Третья модель поднимает узел, который «задумывается» на три секунды:
1. без таймаута : 0x1263b4c | ждали 3000 мс
2. с таймаутом : TimeoutError | The operation was aborted due to timeout | ждали 500 мс
3. попытка 1 : TimeoutError — The operation was aborted due to timeout
3. попытка 2 : TimeoutError — The operation was aborted due to timeout
3. попытка 3 : получилось, ответ 0x1263b4c
итого : 1600 мс на три попытки
Первая строка обманчиво успешна: ответ пришёл. Но узел мог не ответить никогда — и скрипт висел бы, пока его не убьют руками. На демонстрации это выглядит как «участник завис».
Ограничение задаётся сигналом прерывания:
const res = await fetch(url, { method: "POST", body, signal: AbortSignal.timeout(500) });
Третий прогон — цикл до трёх попыток с ограничением и паузой между ними. Две первые упали, третья вернула ответ; всё вместе — 1600 мс.
Это и есть рабочая заготовка для скриптов: ограничение по времени плюс несколько попыток с паузой. Узел, который поднимается в соседнем окне, отвечает не сразу — и без повтора первый же скрипт упадёт на пустом месте.
Что здесь не ловит компилятор
Разбор тела ответа возвращает «что угодно» — и после этой строки компилятор замолкает совсем: обращение к любому вымышленному полю проходит проверку без единого замечания.
Это ровно тот случай, ради которого в главе про свои типы был unknown. Дисциплина простая: результат разбора сразу объявляют «неизвестным» и проверяют форму перед использованием. Иначе ошибка узла или смена формата ответа обнаружится падением где-то дальше по скрипту.
Типичные ошибки
Забыто ожидание перед разбором тела. В переменной окажется обещание, а не данные.
Проверен только статус ответа. Отказ узла лежит в теле и пройдёт незамеченным.
Неуспешный статус принят за исключение. Запрос считается выполненным, если сервер ответил.
Суммы превращены в обычные числа. Тихая потеря точности на больших балансах.
Нет ограничения по времени. Скрипт висит на зависшем узле.
Нет повтора. Узел, который ещё поднимается, роняет скрипт на пустом месте.
Пачка транзакций с одного адреса разом. Номера перепутаются.
Результат разбора не типизирован. Компилятор замолкает, и ошибка формата всплывает позже.
Отсутствует заголовок с типом содержимого. Некоторые узлы отвечают отказом на такой запрос.
Слова главы
- Метод запроса, адрес, заголовки, тело — четыре части обращения к серверу.
- Статус ответа — 200 успех; неуспешный статус исключения не вызывает.
- Формат обмена данными — текст; превращение в объект и обратно — две функции.
- Форма вызова узла — версия, метод, параметры, номер; ответ с результатом или с ошибкой.
- Шестнадцатеричные строки — так приходят числа; суммы превращают в большие целые.
- Две породы отказов — сеть молчит и узел сказал «нет».
- Параллельный запуск — все запросы сразу; порядок результатов сохраняется.
- Ограничение по времени — сигнал прерывания; по умолчанию его нет.
- Повтор с паузой — рабочая заготовка для скриптов.
Тренажёр: набрать вслепую
Цель: скорость ≥ 100 зн/мин, точность ≥ 88%
const res = await fetch(url, { method: "POST", headers, body });
На чемпионате
Разговор с узлом всплывает в задании четырьмя способами, и все четыре — это код из этой главы:
- исполняемый файл запуска решения, который поднимает частную сеть и публикует контракты: узел после старта отвечает не сразу, значит нужен повтор с паузой;
- шаги демонстрации — каждый «показать изменения всех затронутых балансов» — это запросы и печать чисел;
- внешняя служба ликвидации, которая по условию задания опрашивает контракт и сравнивает отношение долга к залогу: цикл опроса с ограничением по времени и повтором;
- дашборд с метриками: там параллельные запросы дают ту самую разницу в десять раз.
Практические выводы для площадки:
- суммы читают как большие целые числа — иначе балансы на демонстрации разойдутся с ключом эксперта в последних разрядах;
- после каждого запроса проверяют и статус, и поле ошибки;
- ограничение по времени ставят всегда: зависший скрипт на демонстрации выглядит хуже, чем скрипт с внятной ошибкой.
Что дальше
Разговор с узлом руками разобран — и это полезно знать, потому что все инструменты делают ровно это, только прячут за удобными именами. Последняя глава блока — про то, как разложить растущий код по файлам: подключение, экспорт и устройство проекта.
Углубиться
- Спецификация формы вызова — три страницы, читаются за двадцать минут; там же формат пакетных запросов.
- Каталог методов узла — что ещё можно спросить, кроме высоты и баланса.
- Подписки на события — постоянное соединение вместо опроса: узел сам сообщает о новом блоке.
- Ограничение частоты — как узлы защищаются от пачек запросов и что возвращают при превышении.
- Кодирование аргументов вызова контракта — то, что инструмент из следующего блока сделает за вас.
Сводная проверка по всей главе: 8 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.
Куда дальше: проверенные ресурсы
- Спецификация JSON-RPC 2.0 — форма запроса и ответа целиком.
- Каталог методов Ethereum JSON-RPC — все методы с примерами.
- MDN: fetch — параметры запроса и работа с ответом.
- MDN: AbortSignal.timeout — ограничение по времени.
- MDN: BigInt — большие целые числа и их ограничения.
Комментарии
Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.