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:
- A pump.fun transaction to demonstrate decoding Anchor program transactions
- 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
- Install dependencies
npm install typescript ts-node @types/node --save-dev
npm install @noble/hashes @solana/web3.js @coral-xyz/anchor@0.29.0
- 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 }
}
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
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
}
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()
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)
}
The individual instructions returned will have the structure below:
{
accounts: PublicKey[],
data: string,
programId: PublicKey
}
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));
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)
})
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")
])
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")
])
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();
}
}
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();
}
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!]);
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()
After running the code, the output should look like this:
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()
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)
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!
You're welcome
Will be releasing the second part soon which will focus on native rust programs
Where can you find the IDL for Raydium v4? I'm struggling with that
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.
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!!
Yes. You can replace the line that fetches the transaction from RPC with your transaction JSON.
If you copy this code all's good except buySellIxs ends being an empty array?
Issue was I was using a handmade sha256 function not the one from @nobles/hashes
Great β¨οΈ