Parsing Solana Program Transactions using Typescript - Part(1/2)
Oluwatobiloba Emmanuel

Oluwatobiloba Emmanuel @teepy

About: Software Engineer | Blockchain | Data

Location:
Somewhere onchain 🌩️⛓️
Joined:
Dec 27, 2020

Parsing Solana Program Transactions using Typescript - Part(1/2)

Publish Date: Oct 28 '24
33 14

Last month, I came across a tweet by kiryl requesting an open-source transaction parser for popular DEFI apps. This led me to develop a minimal open-source parser and inspired this article. The goal is to demonstrate how to parse any Solana program transaction to extract meaningful information in a human-readable format.
To achieve this, we'll analyze two real-world examples:

  1. A pump.fun transaction to demonstrate decoding Anchor program transactions
  2. A Raydium transaction to showcase parsing native Rust program transactions

This part of the series will focus on parsing transactions from Anchor programs.

Project Setup

  • Create a project directory named parser and initialize a new typescript project
mkdir parser
cd parser
npm init
npx tsc --init
Enter fullscreen mode Exit fullscreen mode
  • Install dependencies
npm install typescript ts-node @types/node --save-dev
npm install @noble/hashes @solana/web3.js @coral-xyz/anchor@0.29.0
Enter fullscreen mode Exit fullscreen mode
  • Create an index.ts file in the project root

Decoding Anchor Program Transactions

Anchor is a popular framework for building and testing Solana programs. Programs written in anchor come with an essential component called the IDL (Interface Definition Language) which specifies the program's public interface.
This component is useful in decoding transactions as it provides information about all program instructions, accounts, and events.
The IDL for the pump.fun program, used in this section, can be found here and follows the structure shown below:

{
  version: string
  name: string
  instructions: []
  events: []
  errors: []
  metadata: { address: string }
}
Enter fullscreen mode Exit fullscreen mode

To successfully decode the program instructions, we need to inspect the instructions and events fields and select the instruction and event of interest.

Create an idl.ts file in your project directory and paste the IDL code into it.

Decoding instruction data

The transaction which will be parsed can be found here

Pump.fun transaction
As show in the image above, ~724,879 SSD tokens were bought for ~0.0796 SOL and we are going to be extracting the following output from this transaction:

{
  solAmount: string,
  tokenAmount: string,
  mint: string,
  type: 'buy' | 'sell',
  trader: string
}
Enter fullscreen mode Exit fullscreen mode

To extract the above output, we need to fetch the transaction from Solana mainnet, filter out the instruction of interest, parse the instruction arguments as well as the events.

Now let's go ahead and fetch the transaction from Solana mainnet.
Copy the following code into your index.ts file and run ts-node index.ts.

import { clusterApiUrl, Connection } from "@solana/web3.js"

const main = async () => {
    const signature = "4XQZckrFKjaLHM68kJH7dpSPo2TCfMkwjYhLdcNRu5QdJTjAEehsS5UMaZKDXADD46d8v4XnuyuvLV36rNRTKhn7";
    const connection = new Connection(clusterApiUrl("mainnet-beta"));
    const transaction = await connection.getParsedTransaction(signature, { maxSupportedTransactionVersion: 0 });
    console.log(transaction)
}
main()
Enter fullscreen mode Exit fullscreen mode

Now that we've inspected the transaction structure, we can get all the pump.fun program instructions from all the several instructions in the transaction:

const main = async () => {
    // ...
    const PumpFunProgram = new PublicKey("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
    const pumpIxs = transaction?.transaction.message.instructions.filter((ix) => ix.programId.equals(PumpFunProgram))
    console.log(pumpIxs)
}
Enter fullscreen mode Exit fullscreen mode

The individual instructions returned will have the structure below:

{
  accounts: PublicKey[],
  data: string,
  programId: PublicKey
}
Enter fullscreen mode Exit fullscreen mode

The data field is a base58 encoded array with two sections: the instruction discriminator (first 8 bytes) and the instruction arguments.
The discriminator is a unique identifier for every instruction in the program, allowing us to filter out the specific instructions of interest.

To derive the discriminators, we need the name of these instructions as specified in the IDL (buy and sell).
The following code shows how to derive the discriminators:

const discriminator = Buffer.from(sha256('global:<instruction name>').slice(0, 8));
Enter fullscreen mode Exit fullscreen mode

We can now filter out the specific instructions:

const buyDiscrimator = Buffer.from(sha256('global:buy').slice(0, 8));
const sellDiscriminator = Buffer.from(sha256('global:sell').slice(0, 8));
const buySellIxs = pumpIxs?.filter(ix =>  {
    const discriminator =  bs58.decode((ix as PartiallyDecodedInstruction).data).subarray(0, 8);
        return discriminator.equals(buyDiscrimator) || discriminator.equals(sellDiscriminator)
    })
Enter fullscreen mode Exit fullscreen mode

Also, we need to create a schema for the buy and sell instruction arguments using borsh. The structure of these arguments as well as the data types can be found in the args field for each specified instruction in the IDL.

const buyIxSchema = borsh.struct([
     borsh.u64("discriminator"),
     borsh.u64("amount"),
     borsh.u64("maxSolCost"),
]);
const sellIxSchema = borsh.struct([
     borsh.u64("discriminator"),
     borsh.u64("amount"),
     borsh.u64("minSolOutput")
]) 
Enter fullscreen mode Exit fullscreen mode

In both schemas, the amount field represents the actual token amount bought or sold (tokenAmount). The maxSolCost denotes the maximum SOL the buyer is willing to spend on tokens, while the minSolOutput is the minimum SOL the trader is willing to accept when selling tokens. Since these two fields do not reflect the actual SOL used in each trade, we won’t be using them.

We can also reduce both schemas into one since we're only interested in the amount and discriminator fields.

const tradeSchema = borsh.struct([
   borsh.u64("discriminator"),
   borsh.u64("amount"),
   borsh.u64("solEstimate")   
])
Enter fullscreen mode Exit fullscreen mode

After deriving the discriminators and schema, we can then proceed to start parsing the instruction data:

const main = async () => {
    // ...
    for (let ix of buySellIxs!) {
        ix = ix as PartiallyDecodedInstruction
        const ixDataArray = bs58.decode((ix as PartiallyDecodedInstruction).data);
        const ixData = tradeSchema.decode(ixDataArray);
        const type = bs58.decode(ix.data).subarray(0, 8).equals(buyDiscrimator) ? 'buy' : 'sell';
        const tokenAmount = ixData.amount.toString();
    }
}
Enter fullscreen mode Exit fullscreen mode

To get the mint and trader output, we need to look at the accounts field of both the buy and sell instructions in the IDL. Both instructions have the mint and user accounts, which represent the token and trader, respectively. These accounts are located at positions 2 and 6 respectively and we can use this position to get the actual accounts from the instruction accounts field.

 for (let ix of buySellIxs!) {
        // ...
        const mint = ix.accounts[2].toBase58();
        const trader = ix.accounts[6].toBase58();
    }
Enter fullscreen mode Exit fullscreen mode

Finally, we can get the solAmount involved in the trade by calculating the sol balance change for the bonding curve account in the instruction.
To achieve this, we need to get the bonding curve account at position 3 (check IDL), find the index of this account in the transaction account keys, and then use this index to find the difference between preBalances and postBalances of the transaction result.

const bondingCurve = ix.accounts[3];
const index = transaction?.transaction.message.accountKeys.findIndex((ix) => ix.pubkey.equals(bondingCurve))
const preBalances = transaction?.meta?.preBalances || [];
const postBalances = transaction?.meta?.postBalances || [];
const solAmount = Math.abs(preBalances[index!] - postBalances[index!]);
Enter fullscreen mode Exit fullscreen mode

The final code should look like this:

import { clusterApiUrl, Connection, PartiallyDecodedInstruction, PublicKey } from "@solana/web3.js"
import { sha256 } from '@noble/hashes/sha256';
import { bs58 } from '@coral-xyz/anchor/dist/cjs/utils/bytes';
import * as borsh from "@coral-xyz/borsh";

const main = async () => {
    const signature = "4XQZckrFKjaLHM68kJH7dpSPo2TCfMkwjYhLdcNRu5QdJTjAEehsS5UMaZKDXADD46d8v4XnuyuvLV36rNRTKhn7";
    const connection = new Connection(clusterApiUrl("mainnet-beta"));
    const transaction = await connection.getParsedTransaction(signature, { maxSupportedTransactionVersion: 0 });

    const PumpFunProgram = new PublicKey("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
    const pumpIxs = transaction?.transaction.message.instructions.filter((ix) => ix.programId.equals(PumpFunProgram))

    const buyDiscrimator = Buffer.from(sha256('global:buy').slice(0, 8));
    const sellDiscriminator = Buffer.from(sha256('global:sell').slice(0, 8));
    const buySellIxs = pumpIxs?.filter(ix =>  {
        const discriminator =  bs58.decode((ix as PartiallyDecodedInstruction).data).subarray(0, 8);
        return discriminator.equals(buyDiscrimator) || discriminator.equals(sellDiscriminator)
    })
    const tradeSchema = borsh.struct([
        borsh.u64("discriminator"),
        borsh.u64("amount"),
        borsh.u64("solAmount")
    ])

    for (let ix of buySellIxs!) {
        ix = ix as PartiallyDecodedInstruction;
        const ixDataArray = bs58.decode(ix.data);
        const ixData = tradeSchema.decode(ixDataArray);
        const type = bs58.decode(ix.data).subarray(0, 8).equals(buyDiscrimator) ? 'buy' : 'sell';
        const tokenAmount = ixData.amount.toString();
        const mint = ix.accounts[2].toBase58();
        const trader = ix.accounts[6].toBase58();

        const bondingCurve = ix.accounts[3];
        const index = transaction?.transaction.message.accountKeys.findIndex((ix) => ix.pubkey.equals(bondingCurve))
        const preBalances = transaction?.meta?.preBalances || [];
        const postBalances = transaction?.meta?.postBalances || [];
        const solAmount = Math.abs(preBalances[index!] - postBalances[index!]);

        console.log("--------- Trade Data ------------")
        console.log(`solAmount: ${solAmount}\ntokenAmount: ${tokenAmount}\ntype: ${type}\nmint: ${mint}\ntrader: ${trader}\n`)
    }
}
main()
Enter fullscreen mode Exit fullscreen mode

After running the code, the output should look like this:
Trade result

Parsing Anchor Events

Alternatively, the pump.fun program emits events as described in the IDL and these events could also be parsed to get the required outputs as well:

import { clusterApiUrl, Connection, PublicKey } from "@solana/web3.js"
import { BorshCoder, EventParser, Idl } from "@coral-xyz/anchor";
import { PumpFunIDL } from './idl';

const parseEvents = async () => {
    const signature = "4XQZckrFKjaLHM68kJH7dpSPo2TCfMkwjYhLdcNRu5QdJTjAEehsS5UMaZKDXADD46d8v4XnuyuvLV36rNRTKhn7";
    const PumpFunProgram = new PublicKey("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
    const connection = new Connection(clusterApiUrl("mainnet-beta"));
    const transaction = await connection.getParsedTransaction(signature, { maxSupportedTransactionVersion: 0 });

    const eventParser = new EventParser(PumpFunProgram, new BorshCoder(PumpFunIDL as unknown as Idl));
    const events = eventParser.parseLogs(transaction?.meta?.logMessages!);
    for (let event of events) {
       console.log("--------- Trade Event Data ------------")
       console.log(`solAmount: ${event.data.solAmount}\ntokenAmount: ${event.data.tokenAmount}\ntype: ${event.data.isBuy ? 'buy' : 'sell'}\nmint: ${event.data.mint}\ntrader: ${event.data.user}\n`)
    }
}
parseEvents()
Enter fullscreen mode Exit fullscreen mode

Trade event data

Conclusion

In this part, we explored how to extract valuable information from Anchor program transactions by decoding instruction and events data.

In the second part, we will focus on parsing Solana native program transactions, using Raydium v4 AMM as an example.

The full code for this article is available here.

If you have any questions, suggestions or issues with the code, you can leave a comment.

Comments 14 total

  • v1rtu0so
    v1rtu0soNov 26, 2024

    This is exactly what I have been looking for.. A straight forward and clear guide on how you can quickly and easily map and parse information from the solana blockchain.

    I have been using IDL mappings and then trying to find the offsets by searching through SDK variables. Thank you for this!

    • Oluwatobiloba Emmanuel
      Oluwatobiloba EmmanuelDec 8, 2024

      You're welcome
      Will be releasing the second part soon which will focus on native rust programs

  • Saba Udzilauri (FailedBlock)
    Saba Udzilauri (FailedBlock)Nov 30, 2024

    If you copy this code all's good except buySellIxs ends being an empty array?

  • Jake, Tri Tran
    Jake, Tri TranDec 9, 2024

    Where can you find the IDL for Raydium v4? I'm struggling with that

    • Oluwatobiloba Emmanuel
      Oluwatobiloba EmmanuelDec 14, 2024

      Raydium V4 does not have an IDL as it was written using native Rust, not Anchor. Will be releasing an article soon on how to parse transactions using Raydium V4.

  • Hasan Saadi
    Hasan SaadiDec 19, 2024

    Great work sir,
    is it possible to make this code decode and parse local JSONfiles of fetched transaction? I'm trying to run all the processing locally and minimize interaction with rpcs, any help would be greatly appreciated!!

    • Oluwatobiloba Emmanuel
      Oluwatobiloba EmmanuelDec 21, 2024

      Yes. You can replace the line that fetches the transaction from RPC with your transaction JSON.

  • vancity
    vancityJan 23, 2025

    does this decode the vdt as well?

    • Oluwatobiloba Emmanuel
      Oluwatobiloba EmmanuelFeb 18, 2025

      vdt type logs are usually emitted by native programs not anchor.
      I've been busy lately but I will push out the second version, which focuses more on the vdt type of logs

  • Grand City
    Grand CityFeb 1, 2025

    where is the second part.. Can't wait to see it.

  • Hamidreza Khorammfar
    Hamidreza KhorammfarFeb 9, 2025

    is there any python version?

    • Oluwatobiloba Emmanuel
      Oluwatobiloba EmmanuelFeb 18, 2025

      no python version yet
      maybe sometimes in the nearest future, i'll create a package for python

Add comment