- Сообщение → что хотел сделать пользователь (подписанное им предложение)
- Метаданные → что произошло на самом деле (результат выполнения)
<Buffer 00 bf a0 e8...>, а не в виде читаемых адресов и подписей.
В этом руководстве показано, как: декодировать двоичные данные в удобочитаемый формат, извлекать значимую информацию и понимать полную историю транзакции — от предложения до выполнения.
Поток в реальном времени без декодирования
Запустите приведённый ниже минимальный клиент. Флаги фильтра исключают голосующие и неуспешные транзакции, а массивaccountInclude ограничивает результаты действиями, затрагивающими идентификатор программы Jupiter.
filters, createdAt, а также ветвь transaction, скрывающая два дочерних элемента:
transaction.transaction.transaction→ подписанное сообщениеtransaction.transaction.meta→ метаданные выполнения
Uint8Array, пока остаётся непрозрачным.
Если запустить скрипт с функцией декодирования, вы увидите фактическую вложенную структуру с читаемыми адресами:
Декодирование двоичных данных
Зачем декодировать? Необработанные данные Laserstream содержат подписи, ключи учётных записей и хеши в виде нечитаемых двоичных объектовUint8Array. Чтобы понять транзакцию, их необходимо преобразовать в строки base58.
Решение: Laserstream использует Yellowstone gRPC со встроенными утилитами декодирования. Вместо отдельных декодеров для каждого типа полей мы используем одну рекурсивную функцию, которая преобразует все двоичные данные в удобочитаемый формат.
Структура транзакции
Теперь, когда декодированные данные видны, рассмотрим две основные части каждого обновления транзакции Laserstream. Как было показано в исходном примере, каждая транзакция содержит два ключевых объекта:- Сообщение (предложение) →
transaction.transaction.transaction→ подписанное сообщение (предложение пользователя) - Метаданные (выполнение) →
transaction.transaction.meta→ метаданные выполнения (ответ валидатора)
Предложение: всё внутри сообщения
Пользователь создаёт сообщение, указывающее, что, кто и до какого момента. Ниже показано, как декодировать каждую часть:Заголовок транзакции
numRequiredSignatures сообщает валидатору, сколько подписей нужно проверить, а два значения numReadonly* отмечают учётные записи, которые среда выполнения может считать доступными только для чтения, что позволяет выполнять операции параллельно.
Словарь ключей учётных записей
accountKeys — это обычный список открытых ключей, используемый как таблица поиска. Каждое последующее целое число в транзакции — programIdIndex и каждый элемент массива accounts в инструкции — ссылается на этот список по индексу, экономя более килобайта на сообщение.
Защита от повторного воспроизведения
recentBlockhash истекает, когда он выходит за пределы последних 150 хешей блоков, то есть примерно через девяносто секунд в основной сети.
Инструкции: фактические команды
- Идентификатор программы (
programIdIndex): указывает на адрес в массивеaccountKeys(например, индекс 10 =ComputeBudget111111111111111111111111111111) - Учётные записи (
accounts): строка в кодировке base58, представляющая индексы учётных записей, к которым обращается эта инструкция - Данные (
data): фактические данные инструкции в кодировке base58
convertBuffers учётные записи отображаются в формате base58, но фактически содержат их индексы (например, "3vtmrQMafzDoG2CBz1iqgXPTnC" декодируется в индексы [21, 19, 12, 17, 2, 6, 1, 22]).
Благодаря такой структуре каждая инструкция не повторяет полные 32-байтовые адреса, а только ссылается на позиции в таблице поиска.
Подписи: подтверждение авторизации
signatures содержит криптографические подписи, подтверждающие, что необходимые учётные записи авторизовали эту транзакцию. Количество подписей должно соответствовать header.numRequiredSignatures.
Поиск по таблице адресов
versioned имеет значение true, появляется addressTableLookups с хранящейся в блокчейне таблицей и двумя списками индексов. Таблицы поиска увеличивают жёсткое ограничение на количество адресов до нескольких десятков, сохраняя размер пакета меньше MTU в 1 232 байта.
Транзакция v1: бюджет вычислений в заголовке
Транзакция v1 (SIMD-0385, Agave 4.2) добавляет в сообщение ещё одно поле:transactionConfig.
instructions транзакции v1 никогда не содержит элемент ComputeBudget111111111111111111111111111111. priorityFee — это общая комиссия в лампортах за всю транзакцию, а не микролампорты за единицу вычислений. Поле null означает, что отправитель не задал это значение. В устаревших сообщениях и сообщениях v0 поле transactionConfig отсутствует, поэтому его наличие указывает на транзакцию v1.
В декодере необходимо проверить два момента:
- Извлечение приоритетной комиссии. Считывайте
transactionConfig.priorityFee, если оно существует, и переходите к сканированию инструкций ComputeBudget только для устаревших транзакций и транзакций v0. Код, который сканирует только инструкции, считает, что каждая транзакция v1 платит нулевую приоритетную комиссию. - Версия Proto.
yellowstone-grpc-proto12.6.0 — первый выпуск с полями v1, аhelius-laserstream0.8.4 (JavaScript), 0.6.3 (Rust) и 0.2.0 (Go) — первые выпуски SDK, созданные на его основе. Более старые версии молча отбрасываютtransactionConfig.
Как всё связано: последовательность
Вот что происходит на базовом уровне:- Создание таблицы поиска:
accountKeysперечисляет все адреса, которые затронет эта транзакция - Задание правил:
headerуказывает количество необходимых подписей и учётные записи, доступные только для чтения - Создание команд: каждый
instructionуказывает на:- программу (через
programIdIndex→accountKeys[index]) - необходимые ей учётные записи (через
accounts→ несколько позицийaccountKeys[index]) - данные инструкции (закодированные в
data)
- программу (через
- Добавление авторизации:
signaturesподтверждает, что необходимые учётные записи одобрили эту транзакцию - Задание срока действия:
recentBlockhashпредотвращает повторное воспроизведение этой транзакции в дальнейшем
Выполнение: всё внутри метаданных
Сообщение показывает, что хотел сделать пользователь, а метаданные — что произошло на самом деле при выполнении транзакции валидаторами.Основные сведения о выполнении
Успех или ошибкаerr: null= успешное выполнениеerr: {...}= ошибка со сведениями о нейfee= количество лампортов, списанных за эту транзакцию
accountKeys по индексу:
- Учётная запись 0: потеряно 15000 лампортов (оплата комиссии)
- Учётная запись 1: получено 1461600 лампортов (создана новая учётная запись)
- Учётная запись 3: получено 2001231920 лампортов (учётная запись программы)
Расширенные сведения о выполнении
Внутренние инструкцииПрактические шаблоны декодирования
Ниже приведены распространённые шаблоны для извлечения полезной информации из декодированных транзакций:Полный пример: декодер обменов Jupiter
Ниже приведён полный пример, который декодирует транзакции обмена Jupiter и извлекает значимую информацию:Основные выводы
- Двухчастная структура: каждая транзакция содержит сообщение (что было запрошено) и метаданные (что произошло на самом деле)
- Декодирование двоичных данных: используйте
bs58.encode(), чтобы преобразовать двоичные поля в читаемые строки base58 - Поиск ключей учётных записей: инструкции ссылаются на учётные записи по индексу в массиве
accountKeys - Отслеживание баланса: сравнивайте
preBalancesиpostBalances, чтобы увидеть изменения - Транзакция v1: при наличии
transactionConfigсчитывайте из него бюджет вычислений и приоритетную комиссию; транзакции v1 не содержат инструкций ComputeBudget