> ## Documentation Index
> Fetch the complete documentation index at: https://www.helius.dev/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Декодирование и разбор данных транзакций

> Узнайте, как декодировать и разбирать данные транзакций из Laserstream, чтобы лучше понимать транзакции Solana.

**При получении данных транзакций из Laserstream обратите внимание на две важные части:**

* **Сообщение** → что хотел сделать пользователь (подписанное им предложение)
* **Метаданные** → что произошло на самом деле (результат выполнения)

**Проблема:** необработанные данные транзакций поступают в виде массивов двоичных байтов, таких как `<Buffer 00 bf a0 e8...>`, а не в виде читаемых адресов и подписей.

**В этом руководстве показано, как:** декодировать двоичные данные в удобочитаемый формат, извлекать значимую информацию и понимать полную историю транзакции — от предложения до выполнения.

***

## Поток в реальном времени без декодирования

Запустите приведённый ниже минимальный клиент. Флаги фильтра исключают голосующие и неуспешные транзакции, а массив `accountInclude` ограничивает результаты действиями, затрагивающими идентификатор программы Jupiter.

```ts [expandable] theme={"system"}
import { subscribe, CommitmentLevel, SubscribeUpdate, LaserstreamConfig } from 'helius-laserstream';

async function runTransactionSubscription() {
  const config: LaserstreamConfig = {
    apiKey: 'YOUR_API_KEY', // from https://dashboard.helius.dev/
    endpoint: 'https://laserstream-mainnet-ewr.helius-rpc.com', // pick the closest region
  };

  const request = {
    transactions: {
      "Jupiter-transactions": {
        vote: false,
        failed: false,
        accountInclude: ['JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4']
      }
    },
    commitment: CommitmentLevel.PROCESSED,
    accounts: {}, slots: {}, transactionsStatus: {}, blocks: {}, blocksMeta: {}, entry: {}, accountsDataSlice: []
  };

  const stream = await subscribe(
    config,
    request,
    (u: SubscribeUpdate) => console.log('💸 Transaction update', u),
    console.error
  );

  console.log(`✅ stream id → ${stream.id}`);
  process.on('SIGINT', () => { stream.cancel(); process.exit(0); });
}
runTransactionSubscription().catch(console.error);
```

Теперь в консоли отображается обёртка: `filters`, `createdAt`, а также ветвь `transaction`, скрывающая два дочерних элемента:

* `transaction.transaction.transaction` → подписанное **сообщение**
* `transaction.transaction.meta` → **метаданные** выполнения

```json theme={"system"}
{
 filters: [ 'Jupiter-transactions' ],
  account: undefined,
  transaction: {
    transaction: {
      signature: <Buffer 00 bf a0 e8 9f cc 84 0c a4 83 e3 97 cd b7 57 e2 2b bc 1d ca 8c a6 1b ce b5 57 d7 47 5e ec 1f 46 ae b2 2d 6a 12 cb 88 48 1d 07 bf f6 b2 d3 a8 0b c9 04 ... 14 more bytes>,
      transaction: [Object],
      meta: [Object],
      index: '1177'
    },
    slot: '351704819'
  },
  transactionStatus: undefined,
  block: undefined,
  blockMeta: undefined,
  entry: undefined,
  ping: undefined,
  pong: undefined,
  createdAt: 2025-07-07T10:58:44.403Z
}
```

Всё, что выглядит как `Uint8Array`, пока остаётся непрозрачным.

Если запустить скрипт с функцией декодирования, вы увидите фактическую вложенную структуру с читаемыми адресами:

```json [expandable] theme={"system"}
{
  "filters": ["Jupiter-transactions"],
  "account": undefined,
  "transaction": {
    "transaction": {
      "signature": "5u62i53R1Hdc4thm6DQTNWNkyypuJJSaXSMwwQDxNqKMaAw62H1Xa3Md7QDhYjoPk5dCPg18fwz83kUR6TrMviTx",
      "transaction": {
        "message": {
          "header": {
            "numRequiredSignatures": 1,
            "numReadonlySignedAccounts": 0,
            "numReadonlyUnsignedAccounts": 8
          },
          "accountKeys": [
            "AF9KFSWQeKVxd3kVvFvysWXmATHyYzrN8zN8GtXn4qTF",
            "G9VzXwhDPQ8KRbQAJN6TyGf2gWukYDAvmnXJhPZFev4f",
            "ES9qPxWQVMRZkobJ9yr3U6XSrXzGNLJdSe6p6fS7b82T",
            "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4",
            "ComputeBudget111111111111111111111111111111",
            "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL",
            "So11111111111111111111111111111111111111112",
            "11111111111111111111111111111111",
            "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
          ],
          "recentBlockhash": "8sGjRxJHLJVWqpHt5UdN8qxtLLdgcnLKBpFj9Qrn5PNF",
          "instructions": [
            {
              "programIdIndex": 4,
              "accounts": [],
              "data": "3bjaAzoXPjbY"
            },
            {
              "programIdIndex": 3,
              "accounts": [0, 1, 2, 5, 6, 7, 8],
              "data": "2L1xoA2KEqBgWfGt3fwFJK8k4FPJRJzYHRgH4R3xC8A7"
            }
          ]
        },
        "signatures": [
          "5u62i53R1Hdc4thm6DQTNWNkyypuJJSaXSMwwQDxNqKMaAw62H1Xa3Md7QDhYjoPk5dCPg18fwz83kUR6TrMviTx"
        ]
      },
      "meta": {
        "err": null,
        "fee": 12500,
        "preBalances": [1075517572, 0, 207594496815, 0, 0, 0, 0, 0, 0],
        "postBalances": [1075502572, 1461600, 207594496815, 2001231920, 2039280, 0, 0, 0, 0],
        "innerInstructions": [
          {
            "index": 1,
            "instructions": [
              {
                "programIdIndex": 5,
                "accounts": [1, 2, 0],
                "data": "3Bxs4h24hBtQy9rw"
              }
            ]
          }
        ],
        "logMessages": [
          "Program ComputeBudget111111111111111111111111111111 invoke [1]",
          "Program ComputeBudget111111111111111111111111111111 success",
          "Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 invoke [1]",
          "Program log: Instruction: Swap",
          "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL invoke [2]",
          "Program log: Create",
          "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [3]",
          "Program log: Instruction: GetAccountDataSize",
          "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 1569 of 242833 compute units",
          "Program return: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA pQAAAAAAAAA=",
          "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success",
          "Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 success"
        ],
        "preTokenBalances": [],
        "postTokenBalances": [],
        "computeUnitsConsumed": 182564
      },
      "index": "1177"
    },
    "slot": "351709933"
  },
  "transactionStatus": undefined,
  "block": undefined,
  "blockMeta": undefined,
  "entry": undefined,
  "ping": undefined,
  "pong": undefined,
  "createdAt": "2025-01-14T10:58:44.403Z"
}
```

***

## Декодирование двоичных данных

**Зачем декодировать?** Необработанные данные Laserstream содержат подписи, ключи учётных записей и хеши в виде нечитаемых двоичных объектов `Uint8Array`. Чтобы понять транзакцию, их необходимо преобразовать в строки base58.

**Решение:** Laserstream использует Yellowstone gRPC со встроенными утилитами декодирования. Вместо отдельных декодеров для каждого типа полей мы используем одну рекурсивную функцию, которая преобразует все двоичные данные в удобочитаемый формат.

```ts [expandable] theme={"system"}
import bs58 from 'bs58';
import { subscribe, CommitmentLevel, SubscribeUpdate, LaserstreamConfig } from 'helius-laserstream';

// Recursive function to convert all Buffer/Uint8Array fields to base58
function convertBuffers(obj: any): any {
  if (!obj) return obj;
  if (Buffer.isBuffer(obj) || obj instanceof Uint8Array) {
    return bs58.encode(obj);
  }
  if (Array.isArray(obj)) {
    return obj.map(item => convertBuffers(item));
  }
  if (typeof obj === 'object') {
    return Object.fromEntries(
      Object.entries(obj).map(([key, value]) => [key, convertBuffers(value)])
    );
  }
  return obj;
}

async function runTransactionSubscription() {
  const config: LaserstreamConfig = {
    apiKey: 'YOUR_API_KEY', // from https://dashboard.helius.dev/
    endpoint: 'https://laserstream-mainnet-ewr.helius-rpc.com', // pick the closest region
  };

  const request = {
    transactions: {
      "Jupiter-transactions": {
        vote: false,
        failed: false,
        accountInclude: ['JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4']
      }
    },
    commitment: CommitmentLevel.PROCESSED,
    accounts: {}, slots: {}, transactionsStatus: {}, blocks: {}, blocksMeta: {}, entry: {}, accountsDataSlice: []
  };

  const stream = await subscribe(
    config,
    request,
    (update: SubscribeUpdate) => {
      if (update.transaction) {
        // Convert all binary fields to human-readable format
        const decodedTransaction = convertBuffers(update.transaction);
        console.log('💸 Decoded transaction:', JSON.stringify(decodedTransaction, null, 2));
        
        // Or process specific fields
        processTransaction(update.transaction);
      }
    },
    console.error
  );

  console.log(`✅ stream id → ${stream.id}`);
  process.on('SIGINT', () => { stream.cancel(); process.exit(0); });
}

function processTransaction(txUpdate: any) {
  const tx = txUpdate.transaction;
  const meta = tx.meta;
  
  console.log('Transaction Details:');
  console.log('- Signature:', bs58.encode(tx.signature));
  console.log('- Slot:', txUpdate.slot);
  console.log('- Success:', meta.err === null);
  console.log('- Fee:', meta.fee, 'lamports');
  console.log('- Compute Units:', meta.computeUnitsConsumed);
  
  // Account keys are already available in the message
  const message = tx.transaction.message;
  if (message.accountKeys) {
    console.log('- Account Keys:');
    message.accountKeys.forEach((key: Uint8Array, index: number) => {
      console.log(`  ${index}: ${bs58.encode(key)}`);
    });
  }
  
  // Log messages are already UTF-8 strings
  if (meta.logMessages && meta.logMessages.length > 0) {
    console.log('- Log Messages:');
    meta.logMessages.forEach((log: string) => {
      console.log(`  ${log}`);
    });
  }
}

runTransactionSubscription();
```

Этот подход задействует встроенное декодирование и при этом обрабатывает двоичные поля, которые необходимо преобразовывать вручную. Структура транзакции уже разобрана — нужно лишь преобразовать двоичные поля в удобочитаемый формат.

***

## Структура транзакции

Теперь, когда декодированные данные видны, рассмотрим две основные части каждого обновления транзакции Laserstream. Как было показано в исходном примере, каждая транзакция содержит два ключевых объекта:

* **Сообщение (предложение)** → `transaction.transaction.transaction` → подписанное сообщение (предложение пользователя)
* **Метаданные (выполнение)** → `transaction.transaction.meta` → метаданные выполнения (ответ валидатора)

Эта двухчастная структура описывает полную историю: что запросил пользователь и что произошло на самом деле. Рассмотрим каждую часть подробнее.

***

## Предложение: всё внутри сообщения

Пользователь создаёт сообщение, указывающее, *что*, *кто* и *до какого момента*. Ниже показано, как декодировать каждую часть:

### Заголовок транзакции

```json theme={"system"}
{
  "header": {
    "numRequiredSignatures": 1,
    "numReadonlySignedAccounts": 0,
    "numReadonlyUnsignedAccounts": 5
  }
}
```

`numRequiredSignatures` сообщает валидатору, сколько подписей нужно проверить, а два значения `numReadonly*` отмечают учётные записи, которые среда выполнения может считать доступными только для чтения, что позволяет выполнять операции параллельно.

### Словарь ключей учётных записей

```json theme={"system"}
{
  "accountKeys": [
    "7YttLkHDoNj9wyDur5pM1ejNaAvT9X4eqaYcHQqtj2G5",
    "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4",
    "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
    "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
    "So11111111111111111111111111111111111111112",
    "11111111111111111111111111111111"
  ]
}
```

`accountKeys` — это обычный список открытых ключей, используемый как таблица поиска. Каждое последующее целое число в транзакции — `programIdIndex` и каждый элемент массива `accounts` в инструкции — ссылается на этот список по индексу, экономя более килобайта на сообщение.

### Защита от повторного воспроизведения

```json theme={"system"}
{
  "recentBlockhash": "8sGjRxJHLJVWqpHt5UdN8qxtLLdgcnLKBpFj9Qrn5PNF"
}
```

Срок действия `recentBlockhash` истекает, когда он выходит за пределы последних 150 хешей блоков, то есть примерно через девяносто секунд в основной сети.

### Инструкции: фактические команды

```json theme={"system"}
{
  "instructions": [
    {
      "programIdIndex": 10,
      "data": "HnkkG7"
    },
    {
      "programIdIndex": 15,
      "accounts": "3vtmrQMafzDoG2CBz1iqgXPTnC",
      "data": "5jRcjdixRUDKQKUEt6oHJ747HCB3vWb5y"
    }
  ]
}
```

Каждая инструкция содержит три ключевые части:

* **Идентификатор программы** (`programIdIndex`): указывает на адрес в массиве `accountKeys` (например, индекс 10 = `ComputeBudget111111111111111111111111111111`)
* **Учётные записи** (`accounts`): строка в кодировке base58, представляющая индексы учётных записей, к которым обращается эта инструкция
* **Данные** (`data`): фактические данные инструкции в кодировке base58

Из-за функции `convertBuffers` учётные записи отображаются в формате base58, но фактически содержат их индексы (например, `"3vtmrQMafzDoG2CBz1iqgXPTnC"` декодируется в индексы \[21, 19, 12, 17, 2, 6, 1, 22]).

Благодаря такой структуре каждая инструкция не повторяет полные 32-байтовые адреса, а только ссылается на позиции в таблице поиска.

### Подписи: подтверждение авторизации

```json theme={"system"}
{
  "signatures": [
    "5u62i53R1Hdc4thm6DQTNWNkyypuJJSaXSMwwQDxNqKMaAw62H1Xa3Md7QDhYjoPk5dCPg18fwz83kUR6TrMviTx"
  ]
}
```

`signatures` содержит криптографические подписи, подтверждающие, что необходимые учётные записи авторизовали эту транзакцию. Количество подписей должно соответствовать `header.numRequiredSignatures`.

### Поиск по таблице адресов

```json theme={"system"}
{
  "addressTableLookups": [
    {
      "accountKey": "5Q544fKrFoe6tsEbD7S8EmxGTJYAKtTVhAW5Q5pge4j1",
      "writableIndexes": [0, 1],
      "readonlyIndexes": [2, 3, 4]
    }
  ],
  "versioned": true
}
```

Если `versioned` имеет значение `true`, появляется `addressTableLookups` с хранящейся в блокчейне таблицей и двумя списками индексов. Таблицы поиска увеличивают жёсткое ограничение на количество адресов до нескольких десятков, сохраняя размер пакета меньше MTU в 1 232 байта.

### Транзакция v1: бюджет вычислений в заголовке

Транзакция v1 ([SIMD-0385](https://github.com/solana-foundation/solana-improvement-documents/blob/main/proposals/0385-transaction-v1.md), Agave 4.2) добавляет в сообщение ещё одно поле: `transactionConfig`.

```json theme={"system"}
{
  "transactionConfig": {
    "computeUnitLimit": 200000,
    "heapSize": null,
    "loadedAccountsDataSizeLimit": 200000,
    "priorityFee": 50000
  },
  "versioned": true
}
```

Транзакция 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](/docs/ru/rpc/transaction-v1).

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

Вот что происходит на базовом уровне:

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

***

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

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

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

**Успех или ошибка**

```json theme={"system"}
{
  "err": null,
  "fee": 12500
}
```

* `err: null` = успешное выполнение
* `err: {...}` = ошибка со сведениями о ней
* `fee` = количество лампортов, списанных за эту транзакцию

**Изменения баланса**

```json theme={"system"}
{
  "preBalances": [1075517572, 0, 207594496815, 0, 0, 0, 0, 0, 0],
  "postBalances": [1075502572, 1461600, 207594496815, 2001231920, 2039280, 0, 0, 0, 0]
}
```

Массивы балансов соответствуют массиву `accountKeys` по индексу:

* Учётная запись 0: потеряно 15000 лампортов (оплата комиссии)
* Учётная запись 1: получено 1461600 лампортов (создана новая учётная запись)
* Учётная запись 3: получено 2001231920 лампортов (учётная запись программы)

**Использование вычислительных ресурсов**

```json theme={"system"}
{
  "computeUnitsConsumed": 182564
}
```

Показывает, какая часть запрошенного бюджета вычислений была использована.

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

**Внутренние инструкции**

```json theme={"system"}
{
  "innerInstructions": [
    {
      "index": 1,
      "instructions": [
        {
          "programIdIndex": 5,
          "accounts": [1, 2, 0],
          "data": "3Bxs4h24hBtQy9rw"
        }
      ]
    }
  ]
}
```

Внутренние инструкции — это дополнительные инструкции, вызванные программами во время выполнения. Они не входят в исходную транзакцию, а запускаются основными инструкциями.

**Сообщения журнала**

```json theme={"system"}
{
  "logMessages": [
    "Program ComputeBudget111111111111111111111111111111 invoke [1]",
    "Program ComputeBudget111111111111111111111111111111 success",
    "Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 invoke [1]",
    "Program log: Instruction: Swap",
    "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL invoke [2]",
    "Program log: Create",
    "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [3]",
    "Program log: Instruction: GetAccountDataSize",
    "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 1569 of 242833 compute units",
    "Program return: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA pQAAAAAAAAA=",
    "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success",
    "Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 success"
  ]
}
```

Сообщения журнала предоставляют хронологическую трассировку выполнения программ, показывая, какие программы были вызваны и какие пользовательские сообщения они записали в журнал.

**Изменения балансов токенов**

```json theme={"system"}
{
  "preTokenBalances": [],
  "postTokenBalances": [
    {
      "accountIndex": 1,
      "mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
      "owner": "7YttLkHDoNj9wyDur5pM1ejNaAvT9X4eqaYcHQqtj2G5",
      "uiTokenAmount": {
        "amount": "1000000",
        "decimals": 6,
        "uiAmount": 1.0,
        "uiAmountString": "1"
      }
    }
  ]
}
```

Изменения балансов токенов показывают состояния учётных записей токенов SPL до и после транзакции, включая удобочитаемые суммы с правильной обработкой десятичных разрядов.

***

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

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

```typescript theme={"system"}
// Transaction Success
function isTransactionSuccessful(meta: any): boolean {
  return meta.err === null;
}

function getTransactionFee(meta: any): number {
  return meta.fee;
}

function getComputeUnitsUsed(meta: any): number {
  return meta.computeUnitsConsumed;
}

// Balance Changes
function getBalanceChanges(meta: any, accountKeys: string[]): Array<{account: string, change: number}> {
  const changes = [];
  
  for (let i = 0; i < meta.preBalances.length; i++) {
    const change = meta.postBalances[i] - meta.preBalances[i];
    if (change !== 0) {
      changes.push({
        account: accountKeys[i],
        change: change
      });
    }
  }
  
  return changes;
}

// Program Calls
function getInvokedPrograms(meta: any, accountKeys: string[]): string[] {
  const programs = new Set<string>();
  
  meta.logMessages.forEach((log: string) => {
    const match = log.match(/Program ([1-9A-HJ-NP-Za-km-z]{32,}) invoke/);
    if (match) {
      programs.add(match[1]);
    }
  });
  
  return Array.from(programs);
}

// Token Transfers
function getTokenTransfers(meta: any): Array<{mint: string, from: string, to: string, amount: number}> {
  const transfers = [];
  
  // Compare pre and post token balances
  const preBalances = new Map();
  const postBalances = new Map();
  
  meta.preTokenBalances.forEach((balance: any) => {
    preBalances.set(balance.accountIndex, balance);
  });
  
  meta.postTokenBalances.forEach((balance: any) => {
    postBalances.set(balance.accountIndex, balance);
  });
  
  // Find changes
  for (const [accountIndex, postBalance] of postBalances) {
    const preBalance = preBalances.get(accountIndex);
    const preAmount = preBalance ? parseInt(preBalance.uiTokenAmount.amount) : 0;
    const postAmount = parseInt(postBalance.uiTokenAmount.amount);
    
    if (preAmount !== postAmount) {
      transfers.push({
        mint: postBalance.mint,
        account: postBalance.owner,
        change: postAmount - preAmount,
        decimals: postBalance.uiTokenAmount.decimals
      });
    }
  }
  
  return transfers;
}
```

***

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

Ниже приведён полный пример, который декодирует транзакции обмена Jupiter и извлекает значимую информацию:

```typescript [expandable] theme={"system"}
import bs58 from 'bs58';
import { subscribe, CommitmentLevel, SubscribeUpdate, LaserstreamConfig } from 'helius-laserstream';

interface SwapInfo {
  signature: string;
  slot: number;
  user: string;
  inputMint: string;
  outputMint: string;
  inputAmount: number;
  outputAmount: number;
  fee: number;
  success: boolean;
}

function convertBuffers(obj: any): any {
  if (!obj) return obj;
  if (Buffer.isBuffer(obj) || obj instanceof Uint8Array) {
    return bs58.encode(obj);
  }
  if (Array.isArray(obj)) {
    return obj.map(item => convertBuffers(item));
  }
  if (typeof obj === 'object') {
    return Object.fromEntries(
      Object.entries(obj).map(([key, value]) => [key, convertBuffers(value)])
    );
  }
  return obj;
}

function decodeJupiterSwap(txUpdate: any): SwapInfo | null {
  const tx = txUpdate.transaction;
  const meta = tx.meta;
  const message = tx.transaction.message;
  
  // Convert binary fields to readable format
  const signature = bs58.encode(tx.signature);
  const accountKeys = message.accountKeys.map((key: any) => bs58.encode(key));
  
  // Check if this is a Jupiter transaction
  const jupiterProgram = "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4";
  if (!accountKeys.includes(jupiterProgram)) {
    return null;
  }
  
  // Extract user (first account is typically the fee payer/user)
  const user = accountKeys[0];
  
  // Get token balance changes
  const tokenChanges = getTokenTransfers(meta);
  
  // Find input (negative change) and output (positive change)
  const inputChange = tokenChanges.find(change => change.change < 0);
  const outputChange = tokenChanges.find(change => change.change > 0);
  
  if (!inputChange || !outputChange) {
    return null;
  }
  
  return {
    signature,
    slot: parseInt(txUpdate.slot),
    user,
    inputMint: inputChange.mint,
    outputMint: outputChange.mint,
    inputAmount: Math.abs(inputChange.change),
    outputAmount: outputChange.change,
    fee: meta.fee,
    success: meta.err === null
  };
}

function getTokenTransfers(meta: any): Array<{mint: string, change: number}> {
  const transfers = [];
  
  const preBalances = new Map();
  const postBalances = new Map();
  
  meta.preTokenBalances.forEach((balance: any) => {
    preBalances.set(balance.accountIndex, balance);
  });
  
  meta.postTokenBalances.forEach((balance: any) => {
    postBalances.set(balance.accountIndex, balance);
  });
  
  for (const [accountIndex, postBalance] of postBalances) {
    const preBalance = preBalances.get(accountIndex);
    const preAmount = preBalance ? parseInt(preBalance.uiTokenAmount.amount) : 0;
    const postAmount = parseInt(postBalance.uiTokenAmount.amount);
    
    if (preAmount !== postAmount) {
      transfers.push({
        mint: postBalance.mint,
        change: postAmount - preAmount
      });
    }
  }
  
  return transfers;
}

async function runJupiterSwapMonitor() {
  const config: LaserstreamConfig = {
    apiKey: 'YOUR_API_KEY', // from https://dashboard.helius.dev/
    endpoint: 'https://laserstream-mainnet-ewr.helius-rpc.com', // pick the closest region
  };

  const request = {
    transactions: {
      "Jupiter-swaps": {
        vote: false,
        failed: false,
        accountInclude: ['JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4']
      }
    },
    commitment: CommitmentLevel.PROCESSED,
    accounts: {}, slots: {}, transactionsStatus: {}, blocks: {}, blocksMeta: {}, entry: {}, accountsDataSlice: []
  };

  const stream = await subscribe(
    config,
    request,
    (update: SubscribeUpdate) => {
      if (update.transaction) {
        const swapInfo = decodeJupiterSwap(update.transaction);
        if (swapInfo) {
          console.log('🔄 Jupiter Swap:');
          console.log(`  User: ${swapInfo.user}`);
          console.log(`  Input: ${swapInfo.inputAmount} of ${swapInfo.inputMint}`);
          console.log(`  Output: ${swapInfo.outputAmount} of ${swapInfo.outputMint}`);
          console.log(`  Fee: ${swapInfo.fee} lamports`);
          console.log(`  Success: ${swapInfo.success}`);
          console.log(`  Signature: ${swapInfo.signature}`);
          console.log('---');
        }
      }
    },
    console.error
  );

  console.log(`✅ Jupiter swap monitor started (id: ${stream.id})`);
  process.on('SIGINT', () => { stream.cancel(); process.exit(0); });
}

runJupiterSwapMonitor().catch(console.error);
```

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

***

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

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

Чтобы понимать транзакции Solana, важно учитывать, что они оптимизированы для эффективности: вместо повторения адресов используются таблицы поиска и индексы, которые минимизируют размер транзакции и повышают плотность информации.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.