> ## 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 프로그램 ID와 관련된 활동으로 결과를 제한합니다.

```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` 배열의 각 요소 -는 이 목록에서 인덱스를 기준으로 다시 가리켜, 메시지당 1킬로바이트 이상을 절약합니다.

### 재생 보호

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

`recentBlockhash`는 마지막 150 블록-해시에서 스크롤이 종료되면 만료됩니다. 메인넷에서는 약 90초입니다.

### 명령: 실제 명령

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

각 명령은 세 가지 주요 부분으로 구성됩니다:

* **프로그램 ID** (`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`는 체인 상의 테이블과 두 개의 인덱스 리스트를 표시합니다. 조회 테이블은 주소 수에 대한 엄격한 제한을 수십 개로 올리면서 패킷을 1,232바이트 MTU 이하로 유지합니다.

### 전체 흐름: 연결 방식

처음부터 다음과 같이 진행됩니다:

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` = 이 트랜잭션에 청구된 lamports

**잔액 변화**

```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 lamports 손실 (수수료 지불)
* 계정 1: 1461600 lamports 획득 (새 계정 생성)
* 계정 3: 2001231920 lamports 획득 (프로그램 계정)

**컴퓨트 사용량**

```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`를 비교하여 무엇이 변경되었는지 확인

Solana 트랜잭션을 이해하는 핵심은 효율성을 위해 설계되었다는 점을 인식하는 것입니다: 주소를 반복하는 대신, 조회 테이블과 인덱스를 사용하여 트랜잭션 크기를 최소화하면서 정보 밀도를 극대화합니다.
