Skip to main content
Laserstream에서 트랜잭션 데이터를 받으면, 주의해야 할 두 가지 중요한 사항이 있습니다:
  • 메시지 → 사용자가 수행하고자 했던 내용 (서명된 제안서)
  • 메타 → 실제로 발생한 내용 (실행 결과)
문제점: 원시 트랜잭션 데이터는 읽을 수 있는 주소 및 서명이 아닌 <Buffer 00 bf a0 e8...>와 같은 이진 바이트 배열로 제공됩니다. 이 가이드는 다음을 보여줍니다: 해당 바이너리 데이터를 사람이 읽을 수 있는 형식으로 디코딩하고, 의미 있는 정보를 추출하며, 제안에서 실행까지의 완전한 트랜잭션 스토리를 이해하는 방법을 설명합니다.

실시간 스트림, 디코딩 없음

아래 최소한의 클라이언트를 실행하세요. 필터 플래그는 투표 및 실패한 트랜잭션을 제거하고, accountInclude 배열은 Jupiter 프로그램 ID와 관련된 활동으로 결과를 제한합니다.
콘솔에는 이제 래퍼가 표시됩니다—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 배열의 각 요소 -는 이 목록에서 인덱스를 기준으로 다시 가리켜, 메시지당 1킬로바이트 이상을 절약합니다.

재생 보호

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

명령: 실제 명령

각 명령은 세 가지 주요 부분으로 구성됩니다:
  • 프로그램 ID (programIdIndex): accountKeys 배열의 주소를 가리킵니다 (예: 인덱스 10 = ComputeBudget111111111111111111111111111111)
  • 계정 (accounts): 이 명령이 터치하는 계정 인덱스를 나타내는 base58-인코딩 문자열
  • 데이터 (data): base58로 인코딩된 실제 명령 데이터
convertBuffers 함수로 인해, 계정은 base58로 보이지만 실제로는 계정 인덱스를 포함합니다 (예: "3vtmrQMafzDoG2CBz1iqgXPTnC"는 [21, 19, 12, 17, 2, 6, 1, 22]로 디코딩됨) 이 설계는 전체 32바이트 주소를 반복하는 대신, 각 명령이 조회 테이블의 위치를 참조하도록 합니다.

서명: 승인 증명

signatures는 필요한 계정이 이 트랜잭션을 승인했음을 증명하는 암호화 서명을 포함합니다. 서명 수는 header.numRequiredSignatures와 일치해야 합니다.

주소 테이블 조회

versionedtrue인 경우, addressTableLookups는 체인 상의 테이블과 두 개의 인덱스 리스트를 표시합니다. 조회 테이블은 주소 수에 대한 엄격한 제한을 수십 개로 올리면서 패킷을 1,232바이트 MTU 이하로 유지합니다.

전체 흐름: 연결 방식

처음부터 다음과 같이 진행됩니다:
  1. 조회 테이블 작성: accountKeys는 이 트랜잭션이 만질 모든 주소를 나열합니다
  2. 규칙 설정: header는 필요한 서명의 수와 읽기 전용 계정을 지정합니다
  3. 명령 생성: 각 instruction는 다음을 가리킵니다:
    • 프로그램 (programIdIndexaccountKeys[index])
    • 필요한 계정 (accounts → 여러 accountKeys[index] 위치)
    • 명령 데이터 (data에 인코딩됨)
  4. 승인 추가: signatures는 필요한 계정이 이 트랜잭션을 승인했음을 증명합니다
  5. 만료 설정: recentBlockhash는 이 트랜잭션이 나중에 재생되지 않도록 보장합니다

실행: 메타 내부 모든 것

메시지가 사용자가 하고자 했던 일을 보여주는 반면, 메타는 검증자들이 트랜잭션을 실행했을 때 실제로 일어난 일을 보여줍니다.

기본 실행 정보

성공/실패
  • err: null = 성공
  • err: {...} = 오류 세부 정보와 함께 실패
  • fee = 이 트랜잭션에 청구된 lamports
잔액 변화
잔액 배열은 accountKeys 배열의 인덱스와 일치합니다:
  • 계정 0: 15000 lamports 손실 (수수료 지불)
  • 계정 1: 1461600 lamports 획득 (새 계정 생성)
  • 계정 3: 2001231920 lamports 획득 (프로그램 계정)
컴퓨트 사용량
요청한 양 중 사용된 컴퓨트 예산을 보여줍니다.

고급 실행 세부 정보

내부 명령
내부 명령은 실행 중에 프로그램이 호출한 추가 명령입니다. 이는 원래 트랜잭션의 일부가 아니지만 주요 명령에 의해 트리거되었습니다. 로그 메시지
로그 메시지는 프로그램이 호출된 프로그램과 출력한 사용자 정의 로그 메시지를 순서대로 추적합니다. 토큰 잔액 변화
토큰 잔액 변화는 SPL 토큰 계정의 전/후 상태를 보여주며, 적절한 소수 처리로 사람이 읽을 수 있는 양을 포함합니다.

실용적인 디코딩 패턴

디코딩된 트랜잭션에서 유용한 정보를 추출하기 위한 일반적인 패턴은 다음과 같습니다:

완전한 예제: Jupiter 스왑 디코더

Jupiter 스왑 트랜잭션을 디코딩하고 의미 있는 정보를 추출하는 완전한 예제입니다:
이 예제는 메시지 디코딩과 메타 분석을 결합하여 복잡한 DeFi 트랜잭션에서 비즈니스 관련 정보를 추출하는 방법을 보여줍니다.

주요 포인트

  • 두 부분 구조: 모든 트랜잭션은 메시지 (요청된 내용)와 메타 (실제로 발생한 내용)로 구성됨
  • 이진 디코딩: bs58.encode()를 사용하여 이진 필드를 읽을 수 있는 base58 문자열로 변환
  • 계정 키 조회: 명령은 accountKeys 배열에서 인덱스로 계정을 참조함
  • 잔액 추적: preBalancespostBalances를 비교하여 무엇이 변경되었는지 확인
Solana 트랜잭션을 이해하는 핵심은 효율성을 위해 설계되었다는 점을 인식하는 것입니다: 주소를 반복하는 대신, 조회 테이블과 인덱스를 사용하여 트랜잭션 크기를 최소화하면서 정보 밀도를 극대화합니다.