트랜잭션 분석

요약

@solana/transaction-introspectiongetTransaction RPC 응답을 실제 주소가 포함된 명령어 목록으로 변환합니다. 계정 인덱스(조회 테이블 주소 포함)를 확인하고, 내부 Cross Program Invocation 명령어를 디코딩하며, 탐색기 순서에 따라 전체 명령어 트리를 순회합니다.

런타임에 _온체인_에서 프로그램이 형제 명령어를 검사하는 방법을 찾고 계신가요? 그것은 Instructions sysvar입니다 - 명령어 분석을 참조하세요. 이 페이지는 RPC 응답으로부터 _오프체인_에서 트랜잭션을 디코딩하는 것에 관한 내용입니다.

getTransaction 응답은 바로 사용할 수 있는 명령어 목록을 제공하지 않습니다. 계정은 숫자 인덱스로 저장되며, 버전 트랜잭션은 해당 계정을 정적 키와 주소 조회 테이블에서 로드된 주소로 분리하고, cross-program invocations(CPIs)에서 발생한 내부 명령어는 트랜잭션 메타데이터 내에 인코딩된 블롭 형태로 전달됩니다.

@solana/transaction-introspection가 이 확인 작업을 대신 수행합니다. 와이어 트랜잭션을 디코딩하고, 모든 계정 인덱스를 해당 Address에 매핑하며, 내부 명령어를 정규화하고, 블록 탐색기와 동일한 외부-내부 순서로 명령어를 반환합니다. 반환된 명령어는 codama 생성 클라이언트(예: @solana-program/*)의 parseXInstruction 헬퍼에 직접 연결됩니다.

Terminal
npm install @solana/transaction-introspection @solana/kit @solana/instructions @solana-program/token

응답 디코딩

와이어 포맷 인코딩으로 트랜잭션을 가져와 응답을 decodeTransactionFromRpcResponse에 전달합니다.

Decode a transaction
import { createSolanaRpc, signature } from "@solana/kit";
import { decodeTransactionFromRpcResponse } from "@solana/transaction-introspection";
const rpc = createSolanaRpc("https://api.mainnet.solana.com");
const txid =
"3jUKrQp1UGq5ih6FTDUUt2kkqUfoG2o4kY5T1DoVHK2tXXDLdxJSXzuJGY4JPoRivgbi45U2bc7LZfMa6C4R3szX";
const rpcTx = await rpc
.getTransaction(signature(txid), {
commitment: "confirmed",
encoding: "base64",
maxSupportedTransactionVersion: 0
})
.send();
if (!rpcTx) throw new Error(`Transaction ${txid} not found`);
const { compiledMessage, loadedAddresses } =
decodeTransactionFromRpcResponse(rpcTx);

이후 코드 스니펫은 이 단계의 rpcTx, compiledMessage, loadedAddresses를 재사용합니다.

v1 트랜잭션 준비하기

v1 형식 이 활성화되면 maxSupportedTransactionVersion: 1을 전달해야 하며, 그렇지 않으면 v1 트랜잭션에서 페치가 실패합니다. decodeTransactionFromRpcResponselegacy, 0, 1을 모두 처리합니다(@solana/kit 7.0 기준). v1 메시지는 ComputeBudget 명령어가 아닌 transactionConfig 객체에 리소스 한도를 포함하므로, 디코딩된 명령어를 스캔하여 컴퓨팅 한도나 우선순위 수수료를 가져오는 코드는 v1에서 아무것도 찾지 못합니다.

base64, base58, 또는 json 인코딩으로 가져오세요. decodeTransactionFromRpcResponsejsonParsed 응답을 거부합니다 - 해당 인코딩은 RPC 노드에서 서버 측으로 명령어를 파싱하도록 요청하므로, 클라이언트에서 디코딩할 내용이 남아 있지 않습니다.

명령어 순회

walkInstructions는 블록 탐색기에 표시되는 순서대로 모든 명령어(외부 및 내부)를 배열로 반환합니다. 즉, 각 외부 명령어 바로 뒤에 해당 CPI가 생성한 내부 명령어들이 따라옵니다. 각 명령어에는 호출 계층 내 위치를 나타내는 trace 필드가 포함됩니다.

Walk every instruction
import { walkInstructions } from "@solana/transaction-introspection";
for (const ix of walkInstructions({
compiledMessage,
loadedAddresses,
meta: rpcTx.meta
})) {
const location =
ix.trace.kind === "outer"
? `outer[${ix.trace.index}]`
: `inner[${ix.trace.outerIndex}/${ix.trace.innerIndex}]`;
console.log(location, ix.programAddress, ix.accounts?.length ?? 0);
}

trace는 판별 유니온입니다. 최상위 명령어의 경우 { kind: "outer", index }, 외부 명령어 아래에 중첩된 CPI 명령어의 경우 { kind: "inner", outerIndex, innerIndex, stackHeight? }입니다.

meta는 선택 사항입니다. null일 수 있는 경우에도 rpcTx.meta를 직접 전달하세요. 메타 정보가 없으면 walkInstructions는 외부 명령어만 반환합니다.

프로그램 클라이언트로 파싱하기

walkInstructions에서 반환된 각 명령어는 ResolvedInstruction이므로, @solana/instructions의 조건자(predicates)와 @solana-program/* 클라이언트가 생성한 identifyXInstruction / parseXInstruction 헬퍼에서 별도의 변환 과정 없이 직접 사용할 수 있습니다.

이 예제는 트랜잭션 내의 모든 Token Program SyncNative 명령어를 감사(audit)합니다. 최상위 레벨에서 실행되었는지 CPI 내부에서 실행되었는지에 관계없이 처리됩니다.

Find and parse SyncNative instructions
import {
isInstructionForProgram,
isInstructionWithAccounts,
isInstructionWithData
} from "@solana/instructions";
import { walkInstructions } from "@solana/transaction-introspection";
import {
identifyTokenInstruction,
parseSyncNativeInstruction,
TOKEN_PROGRAM_ADDRESS,
TokenInstruction
} from "@solana-program/token";
for (const ix of walkInstructions({
compiledMessage,
loadedAddresses,
meta: rpcTx.meta
})) {
if (!isInstructionForProgram(ix, TOKEN_PROGRAM_ADDRESS)) continue;
if (!isInstructionWithData(ix) || !isInstructionWithAccounts(ix)) continue;
if (identifyTokenInstruction(ix) !== TokenInstruction.SyncNative) continue;
const parsed = parseSyncNativeInstruction(ix);
console.log(ix.trace, parsed);
}

하위 수준 헬퍼

walkInstructions는 대부분의 요구 사항을 충족하지만, 패키지는 이를 구성하는 하위 요소들도 함께 제공합니다:

  • getInstructionsFromCompiledTransactionMessage(compiledMessage, loadedAddresses?)는 계정이 확인되고 데이터가 디코딩된 외부 명령어만 반환합니다.
  • getInnerInstructionsFromMeta(meta, accountMetas)는 트랜잭션 메타데이터에서 내부 CPI 명령어를 디코딩합니다.
  • getAccountMetasFromCompiledTransactionMessage(compiledMessage, loadedAddresses?)는 트랜잭션 순서대로 전체 계정 목록을 반환합니다. 이는 이전 헬퍼가 내부 명령어 계정 인덱스를 확인하는 데 필요한 입력값입니다.
Resolve outer and inner instructions separately
import {
getAccountMetasFromCompiledTransactionMessage,
getInnerInstructionsFromMeta,
getInstructionsFromCompiledTransactionMessage
} from "@solana/transaction-introspection";
const outer = getInstructionsFromCompiledTransactionMessage(
compiledMessage,
loadedAddresses
);
const accountMetas = getAccountMetasFromCompiledTransactionMessage(
compiledMessage,
loadedAddresses
);
if (!rpcTx.meta) throw new Error("Transaction metadata missing");
const inner = getInnerInstructionsFromMeta(rpcTx.meta, accountMetas);

Rust를 사용하시나요?

이 패키지에 해당하는 Rust 버전은 없습니다. Rust에서 파싱된 명령어를 가져오려면 getTransaction에서 UiTransactionEncoding::JsonParsed를 요청하여 RPC 노드가 계정과 내부 명령어를 대신 확인하도록 하세요.

참고 항목

Is this page helpful?

목차

페이지 편집