Skip to main content
При получении данных транзакций из Laserstream обратите внимание на две важные части:
  • Сообщение → что хотел сделать пользователь (подписанное им предложение)
  • Метаданные → что произошло на самом деле (результат выполнения)
Проблема: необработанные данные транзакций поступают в виде массивов двоичных байтов, таких как <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.
Транзакция v1 содержит бюджет вычислений здесь, а не в инструкциях программы ComputeBudget, поэтому массив instructions транзакции v1 никогда не содержит элемент ComputeBudget111111111111111111111111111111. priorityFee — это общая комиссия в лампортах за всю транзакцию, а не микролампорты за единицу вычислений. Поле null означает, что отправитель не задал это значение. В устаревших сообщениях и сообщениях v0 поле transactionConfig отсутствует, поэтому его наличие указывает на транзакцию v1. В декодере необходимо проверить два момента:
  • Извлечение приоритетной комиссии. Считывайте transactionConfig.priorityFee, если оно существует, и переходите к сканированию инструкций ComputeBudget только для устаревших транзакций и транзакций v0. Код, который сканирует только инструкции, считает, что каждая транзакция v1 платит нулевую приоритетную комиссию.
  • Версия Proto. yellowstone-grpc-proto 12.6.0 — первый выпуск с полями v1, а helius-laserstream 0.8.4 (JavaScript), 0.6.3 (Rust) и 0.2.0 (Go) — первые выпуски SDK, созданные на его основе. Более старые версии молча отбрасывают transactionConfig.
Полный список изменений приведён в разделе Поддержка транзакций v1.

Как всё связано: последовательность

Вот что происходит на базовом уровне:
  1. Создание таблицы поиска: accountKeys перечисляет все адреса, которые затронет эта транзакция
  2. Задание правил: header указывает количество необходимых подписей и учётные записи, доступные только для чтения
  3. Создание команд: каждый instruction указывает на:
    • программу (через programIdIndex → accountKeys[index])
    • необходимые ей учётные записи (через accounts → несколько позиций accountKeys[index])
    • данные инструкции (закодированные в data)
  4. Добавление авторизации: signatures подтверждает, что необходимые учётные записи одобрили эту транзакцию
  5. Задание срока действия: recentBlockhash предотвращает повторное воспроизведение этой транзакции в дальнейшем

Выполнение: всё внутри метаданных

Сообщение показывает, что хотел сделать пользователь, а метаданные — что произошло на самом деле при выполнении транзакции валидаторами.

Основные сведения о выполнении

Успех или ошибка
  • err: null = успешное выполнение
  • err: {...} = ошибка со сведениями о ней
  • fee = количество лампортов, списанных за эту транзакцию
Изменения баланса
Массивы балансов соответствуют массиву accountKeys по индексу:
  • Учётная запись 0: потеряно 15000 лампортов (оплата комиссии)
  • Учётная запись 1: получено 1461600 лампортов (создана новая учётная запись)
  • Учётная запись 3: получено 2001231920 лампортов (учётная запись программы)
Использование вычислительных ресурсов
Показывает, какая часть запрошенного бюджета вычислений была использована.

Расширенные сведения о выполнении

Внутренние инструкции
Внутренние инструкции — это дополнительные инструкции, вызванные программами во время выполнения. Они не входят в исходную транзакцию, а запускаются основными инструкциями. Сообщения журнала
Сообщения журнала предоставляют хронологическую трассировку выполнения программ, показывая, какие программы были вызваны и какие пользовательские сообщения они записали в журнал. Изменения балансов токенов
Изменения балансов токенов показывают состояния учётных записей токенов SPL до и после транзакции, включая удобочитаемые суммы с правильной обработкой десятичных разрядов.

Практические шаблоны декодирования

Ниже приведены распространённые шаблоны для извлечения полезной информации из декодированных транзакций:

Полный пример: декодер обменов Jupiter

Ниже приведён полный пример, который декодирует транзакции обмена Jupiter и извлекает значимую информацию:
Этот пример показывает, как объединить декодирование сообщения с анализом метаданных, чтобы извлекать бизнес-значимую информацию из сложных DeFi-транзакций.

Основные выводы

  • Двухчастная структура: каждая транзакция содержит сообщение (что было запрошено) и метаданные (что произошло на самом деле)
  • Декодирование двоичных данных: используйте bs58.encode(), чтобы преобразовать двоичные поля в читаемые строки base58
  • Поиск ключей учётных записей: инструкции ссылаются на учётные записи по индексу в массиве accountKeys
  • Отслеживание баланса: сравнивайте preBalances и postBalances, чтобы увидеть изменения
  • Транзакция v1: при наличии transactionConfig считывайте из него бюджет вычислений и приоритетную комиссию; транзакции v1 не содержат инструкций ComputeBudget
Чтобы понимать транзакции Solana, важно учитывать, что они оптимизированы для эффективности: вместо повторения адресов используются таблицы поиска и индексы, которые минимизируют размер транзакции и повышают плотность информации.