Skip to main content

Borsh Parsing for Solana Instructions and Logs

When working with Solana transactions, you need to decode Borsh-encoded data to extract events and instruction data. This guide explains the three main approaches used in the codebase, each suited to different protocol implementations.
Why it matters: Different Solana protocols emit events and instructions in different formats. Understanding which parsing method to use is crucial for correctly extracting swap data, liquidity events, and other on-chain information.

Overview: Three Parsing Methods

There are three primary methods to parse Borsh-encoded data from Solana transactions:
  1. parseLogs() - Parse events directly from transaction log messages
  2. borsh.events.decode() - Decode events from instruction data
  3. borsh.instruction.decode() - Decode instruction data itself
The method you use depends on how the protocol implements event emission. Some protocols emit events in transaction logs, while others embed events within instruction data.

Method 1: parseLogs() - Event Parser from Logs

This method parses events directly from the transaction’s log messages. It’s the most straightforward approach when protocols emit events as log entries.

How it works

The EventParser from Anchor reads the transaction’s logMessages array and extracts structured event data based on the program’s IDL.

Example Implementation

import { getSolanaParser } from '../getSolanaParser.ts';

const logMessages = tx.meta?.logMessages || [];
const eventParser = getSolanaParser(PROGRAM_ID, 'event');
const parsedEvents = eventParser.parseLogs(logMessages);

for (const e of parsedEvents) {
  if (e.name === 'SwapEvent') {
    const eventData = e.data;
    // Process swap event data
  }
}

When to use

  • Events are emitted as log messages in the transaction
  • The protocol uses Anchor’s standard event emission
  • You need to parse multiple events from a single transaction

Characteristics

  • Works directly with tx.meta.logMessages
  • No need to decode instruction data manually
  • Events are already structured according to the IDL
  • Can parse multiple events from a single transaction

Method 2: borsh.events.decode() - Decode Events from Instruction Data

This method decodes events that are embedded within instruction data. You extract the event portion from the instruction bytes and decode it separately.

How it works

  1. Decode the instruction data from base58
  2. Skip the first 8 bytes (instruction discriminator)
  3. Extract the remaining bytes as event data
  4. Encode to base64 and decode using borsh.events.decode()

Example Implementation

import { utils } from '@coral-xyz/anchor';
import { base64 } from '@coral-xyz/anchor/dist/cjs/utils/bytes/index.js';
import { getSolanaParser } from '../getSolanaParser.ts';

const borsh = getSolanaParser(PROGRAM_ID, 'borsh', true);

// Decode instruction data
const ixData = utils.bytes.bs58.decode(instruction.data);
// Skip first 8 bytes (instruction discriminator)
const eventData = ixData.subarray(8);
// Encode to base64 for decoding
const eventDataString = base64.encode(eventData);
// Decode the event
const event = borsh.events.decode(eventDataString);

When to use

  • Events are embedded within instruction data
  • The protocol doesn’t emit events as separate log messages
  • You need to extract events from CPI (Cross-Program Invocation) calls

Characteristics

  • Requires manual extraction of event data from instruction bytes
  • Must skip the instruction discriminator (first 8 bytes)
  • Events are encoded within the instruction data structure
  • Useful for parsing events from inner instructions

Method 3: borsh.instruction.decode() - Decode Instruction Data

This method decodes the instruction itself, not events. It’s used when you need to extract instruction parameters or when instructions contain the data you need directly.

How it works

  1. Decode the instruction data from base58
  2. Convert to Buffer format
  3. Decode using borsh.instruction.decode() with the appropriate format (‘hex’ or ‘base64’)

Example Implementation

import { utils } from '@coral-xyz/anchor';
import { getSolanaParser } from '../getSolanaParser.ts';

const borsh = getSolanaParser(PROGRAM_ID, 'borsh', true);

// Decode instruction data
const decoded = utils.bytes.bs58.decode(instruction.data);
const ixData = Buffer.from(decoded.buffer, decoded.byteOffset, decoded.byteLength);
// Decode the instruction
const decodedInstruction = borsh.instruction.decode(ixData, 'hex') as {
  name: string;
  data: Record<string, unknown>;
};

When to use

  • You need instruction parameters, not events
  • The protocol embeds data directly in instructions
  • You’re detecting instruction types (e.g., swap, deposit, withdraw)
  • You need to identify pool creation or migration instructions

Characteristics

  • Decodes the entire instruction structure
  • Provides instruction name and parameters
  • Useful for instruction-based detection (e.g., pool creation)
  • Can be combined with event decoding for comprehensive parsing

Fundamental Borsh Differences

At the core Borsh level, the three methods differ in how they handle the instruction discriminator and data structure:

Instruction Discriminator (8 bytes)

In Solana, all Anchor instructions start with an 8-byte discriminator that identifies the instruction type. This discriminator is computed from the instruction name using SHA256. Structure of instruction data:
[8 bytes: instruction discriminator][N bytes: instruction parameters]

Differences at the Borsh Level

  1. parseLogs() - Event Parser
    • Does NOT deal with instruction data directly
    • Events are emitted by Anchor’s runtime as structured log messages
    • The EventParser reads these log messages (which are already formatted strings)
    • No discriminator handling needed - logs are pre-processed by Anchor
  2. borsh.events.decode() - Event from Instruction Data
    • Must skip the 8-byte instruction discriminator
    • The event data is embedded after the discriminator in the instruction bytes
    • You extract bytes 8+ and decode them as a Borsh event
    • The discriminator identifies the instruction, not the event
  3. borsh.instruction.decode() - Full Instruction Decode
    • Includes the 8-byte discriminator in the decode process
    • Decodes the entire instruction structure: discriminator + parameters
    • Returns both the instruction name (from discriminator) and parameter data
    • Used when you need instruction parameters, not events

Why This Matters

The fundamental difference is where the data lives and how Borsh deserializes it:
  • Logs: Events are serialized by Anchor’s runtime and written as log messages. The EventParser deserializes from these pre-formatted strings.
  • Instruction data (events): Events are Borsh-encoded within instruction bytes, but you must skip the instruction discriminator to reach the event data.
  • Instruction data (full): The entire instruction (discriminator + params) is Borsh-encoded together and decoded as one structure.

Key Differences Summary

MethodData SourceBorsh StructureDiscriminator Handling
parseLogs()tx.meta.logMessagesPre-formatted log stringsNot applicable (logs are already processed)
borsh.events.decode()Instruction data (bytes 8+)Event data only (after discriminator)Must skip first 8 bytes
borsh.instruction.decode()Instruction data (full)Discriminator + parametersIncluded in decode

Protocol Implementation Differences

Different protocols implement event emission differently: The implementation approach depends on the protocol’s architecture and how it structures its on-chain data. Always refer to the protocol’s IDL to determine which method is appropriate.