solana-dex-parser
Version:
Solana Dex Transaction Parser
468 lines (414 loc) • 14.6 kB
text/typescript
import { MessageV0, PublicKey, TokenAmount } from '@solana/web3.js';
import { SPL_TOKEN_INSTRUCTION_TYPES, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID, TOKENS } from './constants';
import { convertToUiAmount, ParseConfig, PoolEventType, SolanaTransaction, TokenInfo } from './types';
import { getInstructionData, getProgramName, getPubkeyString } from './utils';
/**
* Adapter for unified transaction data access
*/
export class TransactionAdapter {
public readonly accountKeys: string[] = [];
public readonly splTokenMap: Map<string, TokenInfo> = new Map();
public readonly splDecimalsMap: Map<string, number> = new Map();
constructor(
private tx: SolanaTransaction,
public config?: ParseConfig
) {
this.accountKeys = this.extractAccountKeys();
this.extractTokenInfo();
}
get txMessage() {
return this.tx.transaction.message as any;
}
get isMessageV0() {
const message = this.tx.transaction.message;
return (
message instanceof MessageV0 ||
('header' in message && 'staticAccountKeys' in message && 'compiledInstructions' in message)
);
}
/**
* Get transaction slot
*/
get slot() {
return this.tx.slot;
}
get version() {
return this.tx.version;
}
/**
* Get transaction block time
*/
get blockTime() {
return this.tx.blockTime || 0;
}
/**
* Get transaction signature
*/
get signature() {
return this.tx.transaction.signatures[0];
}
/**
* Get all instructions
*/
get instructions() {
return this.txMessage.instructions || this.txMessage.compiledInstructions;
}
/**
* Get inner instructions
*/
get innerInstructions() {
return this.tx.meta?.innerInstructions;
}
/**
* Get pre balances
*/
get preBalances() {
return this.tx.meta?.preBalances;
}
/**
* Get post balances
*/
get postBalances() {
return this.tx.meta?.postBalances;
}
/**
* Get pre token balances
*/
get preTokenBalances() {
return this.tx.meta?.preTokenBalances;
}
/**
* Get post token balances
*/
get postTokenBalances() {
return this.tx.meta?.postTokenBalances;
}
/**
* Get first signer account
*/
get signer(): string {
return this.getAccountKey(0);
}
extractAccountKeys() {
if (this.isMessageV0) {
const keys = this.txMessage.staticAccountKeys.map((it: any) => getPubkeyString(it)) || [];
const key2 = this.tx.meta?.loadedAddresses?.writable.map((it) => getPubkeyString(it)) || [];
const key3 = this.tx.meta?.loadedAddresses?.readonly.map((it) => getPubkeyString(it)) || [];
return [...keys, ...key2, ...key3];
} else if (this.version == 0) {
const keys = this.getAccountKeys(this.txMessage.accountKeys) || [];
const key2 = this.getAccountKeys(this.tx.meta?.loadedAddresses?.writable ?? []) || [];
const key3 = this.getAccountKeys(this.tx.meta?.loadedAddresses?.readonly ?? []) || [];
return [...keys, ...key2, ...key3];
} else {
return this.getAccountKeys(this.txMessage.accountKeys) || [];
}
}
/**
* Get unified instruction data
*/
getInstruction(instruction: any) {
const isParsed = !this.isCompiledInstruction(instruction);
return {
programId: isParsed ? getPubkeyString(instruction.programId) : this.accountKeys[instruction.programIdIndex],
accounts: this.getInstructionAccounts(instruction),
data: 'data' in instruction ? instruction.data : '',
parsed: 'parsed' in instruction ? instruction.parsed : undefined,
program: instruction.program || '',
};
}
getInnerInstruction(outerIndex: number, innterIndex: number) {
return this.innerInstructions?.find((it) => it.index == outerIndex)?.instructions[innterIndex];
}
getAccountKeys(accounts: any[]): string[] {
return accounts?.map((it: any) => {
if (it instanceof PublicKey) return it.toBase58();
if (typeof it == 'string') return it;
if (typeof it == 'number') return this.accountKeys[it];
if ('pubkey' in it) return getPubkeyString(it.pubkey);
return it;
});
}
getInstructionAccounts(instruction: any): string[] {
const accounts = instruction.accounts || instruction.accountKeyIndexes;
return this.getAccountKeys(accounts);
}
/**
* Check if instruction is Compiled
*/
isCompiledInstruction(instruction: any): boolean {
return 'programIdIndex' in instruction && !('parsed' in instruction);
}
/**
* Get instruction type
* returns string name if instruction Parsed, e.g. 'transfer';
* returns number if instruction is Compiled, e.g. 3
*/
getInstructionType(instruction: any): string | undefined {
if ('parsed' in instruction && instruction.parsed) {
return instruction.parsed.type; // string name, e.g. 'transfer'
}
// For compiled instructions, try to decode type from data
const data = getInstructionData(instruction);
return data.length > 0 ? data[0].toString() : undefined; // number, e.g. 3
}
/**
* Get account key by index
*/
getAccountKey(index: number): string {
return this.accountKeys[index];
}
getAccountIndex(address: string): number {
return this.accountKeys.findIndex((it) => it == address);
}
/**
* Get token account owner
*/
getTokenAccountOwner(accountKey: string): string | undefined {
const accountInfo = this.tx.meta?.postTokenBalances?.find(
(balance) => this.accountKeys[balance.accountIndex] === accountKey
);
if (accountInfo) {
return accountInfo.owner;
}
return undefined;
}
getAccountBalance(accountKeys: string[]): (TokenAmount | undefined)[] {
return accountKeys.map((accountKey) => {
if (accountKey == '') return undefined;
const index = this.accountKeys.findIndex((it) => it == accountKey);
if (index == -1) return undefined;
const amount = this.tx.meta?.postBalances[index] || 0;
return {
amount: amount.toString(),
uiAmount: convertToUiAmount(amount.toString()),
decimals: 9,
};
});
}
getAccountPreBalance(accountKeys: string[]): (TokenAmount | undefined)[] {
return accountKeys.map((accountKey) => {
if (accountKey == '') return undefined;
const index = this.accountKeys.findIndex((it) => it == accountKey);
if (index == -1) return undefined;
const amount = this.tx.meta?.preBalances[index] || 0;
return {
amount: amount.toString(),
uiAmount: convertToUiAmount(amount.toString()),
decimals: 9,
};
});
}
getTokenAccountBalance(accountKeys: string[]): (TokenAmount | undefined)[] {
return accountKeys.map((accountKey) =>
accountKey == ''
? undefined
: this.tx.meta?.postTokenBalances?.find((balance) => this.accountKeys[balance.accountIndex] === accountKey)
?.uiTokenAmount
);
}
getTokenAccountPreBalance(accountKeys: string[]): (TokenAmount | undefined)[] {
return accountKeys.map((accountKey) =>
accountKey == ''
? undefined
: this.tx.meta?.preTokenBalances?.find((balance) => this.accountKeys[balance.accountIndex] === accountKey)
?.uiTokenAmount
);
}
private readonly defaultSolInfo: TokenInfo = {
mint: TOKENS.SOL,
amount: 0,
amountRaw: '0',
decimals: 9,
};
/**
* Check if token is supported
*/
isSupportedToken(mint: string): boolean {
return Object.values(TOKENS).includes(mint);
}
/**
* Get program ID from instruction
*/
getInstructionProgramId(instruction: any): string {
const ix = this.getInstruction(instruction);
return ix.programId;
}
getTokenDecimals(mint: string): number {
return (
this.preTokenBalances?.find((b) => b.mint === mint)?.uiTokenAmount?.decimals ||
this.postTokenBalances?.find((b) => b.mint === mint)?.uiTokenAmount?.decimals ||
9
);
}
/**
* Create base pool event data
* @param type - Type of pool event
* @param tx - The parsed transaction with metadata
* @param programId - The program ID associated with the event
* @returns Base pool event object
*/
getPoolEventBase = (type: PoolEventType, programId: string) => ({
user: this.signer,
type,
programId,
amm: getProgramName(programId),
slot: this.slot,
timestamp: this.blockTime,
signature: this.signature,
});
/**
* Extract token information from transaction
*/
private extractTokenInfo() {
// Process token balances
this.extractTokenBalances();
// Process transfer instructions for additional token info
this.extractTokenFromInstructions();
// Add SOL token info if not exists
if (!this.splTokenMap.has(TOKENS.SOL)) {
this.splTokenMap.set(TOKENS.SOL, this.defaultSolInfo);
}
if (!this.splDecimalsMap.has(TOKENS.SOL)) {
this.splDecimalsMap.set(TOKENS.SOL, this.defaultSolInfo.decimals);
}
}
/**
* Extract token balances from pre and post states
*/
private extractTokenBalances() {
const postBalances = this.postTokenBalances || [];
postBalances.forEach((balance) => {
if (!balance.mint) return;
const accountKey = this.accountKeys[balance.accountIndex];
if (!this.splTokenMap.has(accountKey)) {
const tokenInfo: TokenInfo = {
mint: balance.mint,
amount: balance.uiTokenAmount.uiAmount || 0,
amountRaw: balance.uiTokenAmount.amount,
decimals: balance.uiTokenAmount.decimals,
};
this.splTokenMap.set(accountKey, tokenInfo);
}
if (!this.splDecimalsMap.has(balance.mint)) {
this.splDecimalsMap.set(balance.mint, balance.uiTokenAmount.decimals);
}
});
}
/**
* Extract token info from transfer instructions
*/
private extractTokenFromInstructions() {
this.instructions.forEach((ix: any) => {
if (this.isCompiledInstruction(ix)) {
this.extractFromCompiledTransfer(ix);
} else {
this.extractFromParsedTransfer(ix);
}
});
// Process inner instructions
this.innerInstructions?.forEach((inner) => {
inner.instructions.forEach((ix) => {
if (this.isCompiledInstruction(ix)) {
this.extractFromCompiledTransfer(ix);
} else {
this.extractFromParsedTransfer(ix);
}
});
});
}
private setTokenInfo(source?: string, destination?: string, mint?: string, decimals?: number) {
if (source) {
if (this.splTokenMap.has(source) && mint && decimals) {
this.splTokenMap.set(source, { mint, amount: 0, amountRaw: '0', decimals });
} else if (!this.splTokenMap.has(source)) {
this.splTokenMap.set(source, {
mint: mint || TOKENS.SOL,
amount: 0,
amountRaw: '0',
decimals: decimals || 9,
});
}
}
if (destination) {
if (this.splTokenMap.has(destination) && mint && decimals) {
this.splTokenMap.set(destination, { mint, amount: 0, amountRaw: '0', decimals });
} else if (!this.splTokenMap.has(destination)) {
this.splTokenMap.set(destination, {
mint: mint || TOKENS.SOL,
amount: 0,
amountRaw: '0',
decimals: decimals || 9,
});
}
}
if (mint && decimals && !this.splDecimalsMap.has(mint)) {
this.splDecimalsMap.set(mint, decimals);
}
}
/**
* Extract token info from parsed transfer instruction
*/
private extractFromParsedTransfer(ix: any) {
if (!ix.parsed || !ix.program) return;
if (ix.programId != TOKEN_PROGRAM_ID && ix.programId != TOKEN_2022_PROGRAM_ID) return;
const { source, destination, mint, decimals } = ix.parsed?.info || {};
if (!source && !destination) return;
this.setTokenInfo(source, destination, mint, decimals);
}
/**
* Extract token info from compiled transfer instruction
*/
private extractFromCompiledTransfer(ix: any) {
const decoded = getInstructionData(ix);
if (!decoded) return;
const programId = this.accountKeys[ix.programIdIndex];
if (programId != TOKEN_PROGRAM_ID && programId != TOKEN_2022_PROGRAM_ID) return;
let source, destination, mint, decimals;
// const amount = decoded.readBigUInt64LE(1);
const accounts = ix.accounts as number[];
if (!accounts) return;
switch (decoded[0]) {
case SPL_TOKEN_INSTRUCTION_TYPES.Transfer:
if (accounts.length < 3) return;
[source, destination] = [this.accountKeys[accounts[0]], this.accountKeys[accounts[1]]]; // source, destination,amount, authority
break;
case SPL_TOKEN_INSTRUCTION_TYPES.TransferChecked:
if (accounts.length < 4) return;
[source, mint, destination] = [
this.accountKeys[accounts[0]],
this.accountKeys[accounts[1]],
this.accountKeys[accounts[2]],
]; // source, mint, destination, authority,amount,decimals
decimals = decoded.readUint8(9);
break;
case SPL_TOKEN_INSTRUCTION_TYPES.InitializeMint:
if (accounts.length < 2) return;
[mint, destination] = [this.accountKeys[accounts[0]], this.accountKeys[accounts[1]]]; // mint, decimals, authority,freezeAuthority
break;
case SPL_TOKEN_INSTRUCTION_TYPES.MintTo:
if (accounts.length < 2) return;
[mint, destination] = [this.accountKeys[accounts[0]], this.accountKeys[accounts[1]]]; // mint, destination, authority, amount
break;
case SPL_TOKEN_INSTRUCTION_TYPES.MintToChecked:
if (accounts.length < 3) return;
[mint, destination] = [this.accountKeys[accounts[0]], this.accountKeys[accounts[1]]]; // mint, destination, authority, amount,decimals
decimals = decoded.readUint8(9);
break;
case SPL_TOKEN_INSTRUCTION_TYPES.Burn:
if (accounts.length < 2) return;
[source, mint] = [this.accountKeys[accounts[0]], this.accountKeys[accounts[1]]]; // account, mint, authority, amount
break;
case SPL_TOKEN_INSTRUCTION_TYPES.BurnChecked:
if (accounts.length < 3) return;
[source, mint] = [this.accountKeys[accounts[0]], this.accountKeys[accounts[1]]]; // account, mint, authority, amount,decimals
decimals = decoded.readUint8(9);
break;
case SPL_TOKEN_INSTRUCTION_TYPES.CloseAccount:
if (accounts.length < 3) return;
[source, destination] = [this.accountKeys[accounts[0]], this.accountKeys[accounts[1]]]; // account, destination, authority
break;
}
this.setTokenInfo(source, destination, mint, decimals);
}
}