solana-dex-parser
Version:
Solana Dex Transaction Parser
460 lines (404 loc) • 14.9 kB
text/typescript
import { DEX_PROGRAMS, SYSTEM_PROGRAMS, TOKENS } from './constants';
import { InstructionClassifier } from './instruction-classifier';
import { TransactionAdapter } from './transaction-adapter';
import {
isCompiledExtraAction,
isCompiledNativeTransfer,
isCompiledTransfer,
isCompiledTransferCheck,
processCompiledExtraAction,
processCompiledNatvieTransfer,
processCompiledTransfer,
processCompiledTransferCheck,
} from './transfer-compiled-utils';
import {
isExtraAction,
isTransfer,
isTransferCheck,
processExtraAction,
processTransfer,
processTransferCheck,
} from './transfer-utils';
import { DexInfo, TokenInfo, TradeInfo, TransferData, TransferInfo } from './types';
import { getTradeType } from './utils';
export class TransactionUtils {
constructor(private adapter: TransactionAdapter) {}
/**
* Get DEX information from transaction
*/
getDexInfo(classifier: InstructionClassifier): DexInfo {
const programIds = classifier.getAllProgramIds();
if (!programIds.length) return {};
for (const programId of programIds) {
const dexProgram = Object.values(DEX_PROGRAMS).find((dex) => dex.id === programId);
if (dexProgram) {
const isRoute = !dexProgram.tags.includes('amm');
return {
programId: dexProgram.id,
route: isRoute ? dexProgram.name : undefined,
amm: !isRoute ? dexProgram.name : undefined,
};
}
}
return { programId: programIds[0] };
}
/**
* Get transfer actions from transaction
*/
getTransferActions(extraTypes?: string[]): Record<string, TransferData[]> {
const actions: Record<string, TransferData[]> = {};
const innerInstructions = this.adapter.innerInstructions;
let groupKey = '';
// process transfers of program instructions
innerInstructions?.forEach((set) => {
const outerIndex = set.index;
const outerInstruction = this.adapter.instructions[outerIndex];
const outerProgramId = this.adapter.getInstructionProgramId(outerInstruction);
if (SYSTEM_PROGRAMS.includes(outerProgramId)) return;
groupKey = `${outerProgramId}:${outerIndex}`;
set.instructions.forEach((ix, innerIndex) => {
const innerProgramId = this.adapter.getInstructionProgramId(ix);
// Special case for meteora vault
if (!SYSTEM_PROGRAMS.includes(innerProgramId) && !this.isIgnoredProgram(innerProgramId)) {
groupKey = `${innerProgramId}:${outerIndex}-${innerIndex}`;
return;
}
const transferData = this.parseInstructionAction(ix, `${outerIndex}-${innerIndex}`, extraTypes);
if (transferData) {
if (actions[groupKey]) {
actions[groupKey].push(transferData);
} else {
actions[groupKey] = [transferData];
}
}
});
});
// process transfers without program
if (Object.keys(actions).length == 0) {
groupKey = 'transfer';
this.adapter.instructions?.forEach((ix: any, outerIndex: any) => {
const transferData = this.parseInstructionAction(ix, `${outerIndex}`, extraTypes);
if (transferData) {
if (actions[groupKey]) {
actions[groupKey].push(transferData);
} else {
actions[groupKey] = [transferData];
}
}
});
}
return actions;
}
processTransferInstructions(outerIndex: number, extraTypes?: string[]): TransferData[] {
const innerInstructions = this.adapter.innerInstructions;
if (!innerInstructions) return [];
return innerInstructions
.filter((set) => set.index === outerIndex)
.flatMap((set) =>
set.instructions
.map((instruction, idx) => {
const items = this.parseInstructionAction(instruction, `${outerIndex}-${idx}`, extraTypes);
return items;
})
.filter((transfer): transfer is TransferData => transfer !== null)
);
}
/**
* Parse instruction actions (both parsed and compiled)
* actions: transfer/transferCheced/mintTo/burn
*/
parseInstructionAction(instruction: any, idx: string, extraTypes?: string[]): TransferData | null {
const ix = this.adapter.getInstruction(instruction);
// Handle parsed instruction
if (ix.parsed) {
return this.parseParsedInstructionAction(ix, idx, extraTypes);
}
// Handle compiled instruction
return this.parseCompiledInstructionAction(ix, idx, extraTypes);
}
/**
* Parse parsed instruction
*/
parseParsedInstructionAction(instruction: any, idx: string, extraTypes?: string[]): TransferData | null {
if (isTransfer(instruction)) {
return processTransfer(instruction, idx, this.adapter);
}
if (isTransferCheck(instruction)) {
return processTransferCheck(instruction, idx, this.adapter);
}
if (extraTypes) {
const actions = extraTypes
.map((it) => {
if (isExtraAction(instruction, it)) {
return processExtraAction(instruction, idx, this.adapter, it);
}
})
.filter((it) => !!it);
return actions.length > 0 ? actions[0] : null;
}
return null;
}
/**
* Parse compiled instruction
*/
parseCompiledInstructionAction(instruction: any, idx: string, extraTypes?: string[]): TransferData | null {
if (isCompiledTransfer(instruction)) {
return processCompiledTransfer(instruction, idx, this.adapter);
}
if (isCompiledNativeTransfer(instruction)) {
return processCompiledNatvieTransfer(instruction, idx, this.adapter);
}
if (isCompiledTransferCheck(instruction)) {
return processCompiledTransferCheck(instruction, idx, this.adapter);
}
if (extraTypes) {
const actions = extraTypes
.map((it) => {
if (isCompiledExtraAction(instruction, it)) {
return processCompiledExtraAction(instruction, idx, this.adapter, it);
}
})
.filter((it) => !!it);
return actions.length > 0 ? actions[0] : null;
}
return null;
}
/**
* Get mint from instruction
*/
getMintFromInstruction(ix: any, info: any): string | undefined {
let mint = this.adapter.splTokenMap.get(info.destination)?.mint;
if (!mint) mint = this.adapter.splTokenMap.get(info.source)?.mint;
if (!mint && ix.programId === TOKENS.NATIVE) mint = TOKENS.SOL;
return mint;
}
/**
* Get token amount from instruction info
*/
getTokenAmount(info: any, decimals: number) {
if (info.tokenAmount) return info.tokenAmount;
const amount = info.amount || info.lamports || '0';
return {
amount,
decimals,
uiAmount: Number(amount) / Math.pow(10, decimals),
};
}
/**
* Check if program should be ignored for grouping
*/
isIgnoredProgram(programId: string): boolean {
return Object.values(DEX_PROGRAMS)
.filter((it) => it.tags.includes('vault'))
.map((it) => it.id)
.includes(programId);
}
/**
* Get transfer info from transfer data
*/
getTransferInfo(transferData: TransferData, timestamp: number, signature: string): TransferInfo | null {
const { info } = transferData;
if (!info || !info.tokenAmount) return null;
const tokenInfo: TokenInfo = {
mint: info.mint || '',
amount: info.tokenAmount.uiAmount,
amountRaw: info.tokenAmount.amount,
decimals: info.tokenAmount.decimals,
};
return {
type: info.source === info.authority ? 'TRANSFER_OUT' : 'TRANSFER_IN',
token: tokenInfo,
from: info.source,
to: info.destination,
timestamp,
signature,
};
}
/**
* Get transfer info list from transfer data
*/
getTransferInfoList(transferDataList: TransferData[]): TransferInfo[] {
const timestamp = this.adapter.blockTime || 0;
const signature = this.adapter.signature;
return transferDataList
.map((data) => this.getTransferInfo(data, timestamp, signature))
.filter((info): info is TransferInfo => info !== null);
}
/**
* Process swap data from transfers
*/
processSwapData(transfers: TransferData[], dexInfo: DexInfo): TradeInfo | null {
if (!transfers.length) {
throw new Error('No swap data provided');
}
const uniqueTokens = this.extractUniqueTokens(transfers);
if (uniqueTokens.length < 2) {
throw `Insufficient unique tokens for swap`;
}
const signer = this.getSwapSigner();
const { inputToken, outputToken } = this.calculateTokenAmounts(signer, transfers, uniqueTokens);
return {
type: getTradeType(inputToken.mint, outputToken.mint),
inputToken,
outputToken,
user: signer,
programId: dexInfo.programId,
amm: dexInfo.amm,
route: dexInfo.route || '',
slot: this.adapter.slot,
timestamp: this.adapter.blockTime || 0,
signature: this.adapter.signature,
idx: transfers[0].idx,
};
}
/**
* Get signer for swap transaction
*/
getSwapSigner(): string {
const defaultSigner = this.adapter.accountKeys[0];
// Check for Jupiter DCA program
const isDCAProgram = this.adapter.accountKeys.find((key) => key === DEX_PROGRAMS.JUPITER_DCA.id);
return isDCAProgram ? this.adapter.accountKeys[2] : defaultSigner;
}
/**
* Extract unique tokens from transfers
*/
private extractUniqueTokens(transfers: TransferData[]): TokenInfo[] {
const uniqueTokens: TokenInfo[] = [];
const seenTokens = new Set<string>();
transfers.forEach((transfer) => {
const tokenInfo = this.getTransferTokenInfo(transfer);
if (tokenInfo && !seenTokens.has(tokenInfo.mint)) {
uniqueTokens.push(tokenInfo);
seenTokens.add(tokenInfo.mint);
}
});
return uniqueTokens;
}
/**
* Calculate token amounts for swap
*/
private calculateTokenAmounts(signer: string, transfers: TransferData[], uniqueTokens: TokenInfo[]) {
let inputToken = uniqueTokens[0];
let outputToken = uniqueTokens[uniqueTokens.length - 1];
if (outputToken.source === signer) {
[inputToken, outputToken] = [outputToken, inputToken];
}
const amounts = this.sumTokenAmounts(transfers, inputToken.mint, outputToken.mint);
return {
inputToken: {
...inputToken,
amount: amounts.inputAmount,
amountRaw: amounts.inputAmountRaw.toString(),
} as TokenInfo,
outputToken: {
...outputToken,
amount: amounts.outputAmount,
amountRaw: amounts.outputAmountRaw.toString(),
},
};
}
/**
* Sum token amounts from transfers
*/
private sumTokenAmounts(transfers: TransferData[], inputMint: string, outputMint: string) {
const seenTransfers = new Set<string>();
let inputAmount: number = 0;
let outputAmount: number = 0;
let inputAmountRaw: bigint = 0n;
let outputAmountRaw: bigint = 0n;
transfers.forEach((transfer) => {
const tokenInfo = this.getTransferTokenInfo(transfer);
if (!tokenInfo) return;
const key = `${tokenInfo.amount}-${tokenInfo.mint}`;
if (seenTransfers.has(key)) return;
seenTransfers.add(key);
if (tokenInfo.mint === inputMint) {
inputAmount += tokenInfo.amount;
inputAmountRaw += BigInt(tokenInfo.amountRaw);
}
if (tokenInfo.mint === outputMint) {
outputAmount += tokenInfo.amount;
outputAmountRaw += BigInt(tokenInfo.amountRaw);
}
});
return { inputAmount, inputAmountRaw, outputAmount, outputAmountRaw };
}
/**
* Get token info from transfer data
*/
getTransferTokenInfo(transfer: TransferData): TokenInfo | null {
return transfer?.info
? {
mint: transfer.info.mint,
amount: transfer.info.tokenAmount.uiAmount,
amountRaw: transfer.info.tokenAmount.amount,
decimals: transfer.info.tokenAmount.decimals,
authority: transfer.info.authority,
destination: transfer.info.destination,
destinationOwner: transfer.info.destinationOwner,
destinationBalance: transfer.info.destinationBalance,
destinationPreBalance: transfer.info.destinationPreBalance,
source: transfer.info.source,
sourceBalance: transfer.info.sourceBalance,
sourcePreBalance: transfer.info.sourcePreBalance,
}
: null;
}
/**
* Sort and get LP tokens
* make sure token0 is SPL Token, token1 is SOL/USDC/USDT
* SOL,USDT > buy
* SOL,DDD > buy
* USDC,USDT/DDD > buy
* USDT,USDC
* DDD,USDC > sell
* USDC,SOL > sell
* USDT,SOL > sell
* @param transfers
* @returns
*/
getLPTransfers = (transfers: TransferData[]) => {
const tokens = transfers.filter((it) => it.type.includes('transfer'));
if (tokens.length >= 2) {
if (
tokens[0].info.mint == TOKENS.SOL ||
(this.adapter.isSupportedToken(tokens[0].info.mint) && !this.adapter.isSupportedToken(tokens[1].info.mint))
) {
return [tokens[1], tokens[0]];
}
}
return tokens;
};
attachTokenTransferInfo = (trade: TradeInfo, transferActions: Record<string, TransferData[]>): TradeInfo => {
const inputTransfer = Object.values(transferActions)
.flat()
.find((it) => it.info.mint == trade.inputToken.mint && it.info.tokenAmount?.amount == trade.inputToken.amountRaw);
const outputTransfer = Object.values(transferActions)
.flat()
.find(
(it) => it.info.mint == trade.outputToken.mint && it.info.tokenAmount?.amount == trade.outputToken.amountRaw
);
if (inputTransfer) {
trade.inputToken.authority = inputTransfer.info.authority;
trade.inputToken.source = inputTransfer.info.source;
trade.inputToken.destination = inputTransfer.info.destination;
trade.inputToken.destinationOwner = inputTransfer.info.destinationOwner;
trade.inputToken.destinationBalance = inputTransfer.info.destinationBalance;
trade.inputToken.destinationPreBalance = inputTransfer.info.destinationPreBalance;
trade.inputToken.sourceBalance = inputTransfer.info.sourceBalance;
trade.inputToken.sourcePreBalance = inputTransfer.info.sourcePreBalance;
}
if (outputTransfer) {
trade.outputToken.authority = outputTransfer.info.authority;
trade.outputToken.source = outputTransfer.info.source;
trade.outputToken.destination = outputTransfer.info.destination;
trade.outputToken.destinationOwner = outputTransfer.info.destinationOwner;
trade.outputToken.destinationBalance = outputTransfer.info.destinationBalance;
trade.outputToken.destinationPreBalance = outputTransfer.info.destinationPreBalance;
trade.outputToken.sourceBalance = outputTransfer.info.sourceBalance;
trade.outputToken.sourcePreBalance = outputTransfer.info.sourcePreBalance;
}
return trade;
};
}