DEV Community

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

Posted on

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

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.

Top comments (9)

Collapse
 
v1rtu0so profile image
v1rtu0so

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!

Collapse
 
teepy profile image
Oluwatobiloba Emmanuel

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

Collapse
 
jjtt profile image
Jake, Tri Tran

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

Collapse
 
teepy profile image
Oluwatobiloba Emmanuel

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.

Collapse
 
itsmxr_0 profile image
Hasan Saadi

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!!

Collapse
 
teepy profile image
Oluwatobiloba Emmanuel

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

Collapse
 
failedblock profile image
Saba Udzilauri (FailedBlock)

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

Collapse
 
failedblock profile image
Saba Udzilauri (FailedBlock)

Issue was I was using a handmade sha256 function not the one from @nobles/hashes

Collapse
 
teepy profile image
Oluwatobiloba Emmanuel

Great ✨️