solana-dex-parser
Version:
Solana Dex Transaction Parser
641 lines • 27.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.TransactionUtils = void 0;
const constants_1 = require("./constants");
const transfer_compiled_utils_1 = require("./transfer-compiled-utils");
const transfer_utils_1 = require("./transfer-utils");
const types_1 = require("./types");
const utils_1 = require("./utils");
class TransactionUtils {
constructor(adapter) {
this.adapter = adapter;
/**
* 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
*/
this.getLPTransfers = (transfers) => {
const tokens = transfers.filter((it) => it.type.includes('transfer'));
if (tokens.length >= 2) {
if (tokens[0].info.mint == constants_1.TOKENS.SOL ||
(this.adapter.isSupportedToken(tokens[0].info.mint) && !this.adapter.isSupportedToken(tokens[1].info.mint))) {
return [tokens[1], tokens[0]];
}
}
return tokens;
};
this.attachTokenTransferInfo = (trade, transferActions) => {
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);
const [solChanges, tokenChanges] = [
this.adapter.getAccountSolBalanceChanges(false),
this.adapter.getAccountTokenBalanceChanges(true),
];
const inputAmt = trade.inputToken.mint == constants_1.TOKENS.SOL
? solChanges.get(trade.user)
: tokenChanges.get(trade.user)?.get(trade.inputToken.mint);
const outputAmt = trade.outputToken.mint == constants_1.TOKENS.SOL
? solChanges.get(trade.user)
: tokenChanges.get(trade.user)?.get(trade.outputToken.mint);
trade.inputToken.balanceChange = (inputAmt?.change?.amount || trade.inputToken.amountRaw).replace('-', ''); // abs value
trade.outputToken.balanceChange = outputAmt?.change?.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;
}
else {
trade.inputToken.sourceBalance = inputAmt?.post;
trade.inputToken.sourcePreBalance = inputAmt?.pre;
}
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;
}
else {
trade.outputToken.destinationBalance = outputAmt?.post;
trade.outputToken.destinationPreBalance = outputAmt?.pre;
}
trade.signer = this.adapter.signers;
return trade;
};
this.attachUserBalanceToLPs = (liquidities) => {
liquidities.forEach((it) => {
const [solChanges, tokenChanges] = [
this.adapter.getAccountSolBalanceChanges(false),
this.adapter.getAccountTokenBalanceChanges(true),
];
const solAmt = solChanges.get(it.user);
const [token0Amt, token1Amt] = [
it.token0Mint == constants_1.TOKENS.SOL ? solAmt : tokenChanges.get(it.user)?.get(it.token0Mint),
it.token1Mint == constants_1.TOKENS.SOL ? solAmt : tokenChanges.get(it.user)?.get(it.token1Mint),
];
it.token0BalanceChange = token0Amt?.change?.amount || it.token0AmountRaw;
it.token1BalanceChange = token1Amt?.change?.amount || it.token1AmountRaw;
it.signer = this.adapter.signers;
});
return liquidities;
};
}
/**
* Get DEX information from transaction
*/
getDexInfo(classifier) {
const programIds = classifier.getAllProgramIds();
if (!programIds.length)
return {};
for (const programId of programIds) {
const dexProgram = Object.values(constants_1.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) {
const actions = {};
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 (constants_1.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 (!constants_1.SYSTEM_PROGRAMS.includes(innerProgramId) && !this.isIgnoredProgram(innerProgramId)) {
groupKey = `${innerProgramId}:${outerIndex}-${innerIndex}`;
return;
}
const transferData = this.parseInstructionAction(ix, `${outerIndex}-${innerIndex}`, extraTypes);
if (transferData) {
if (constants_1.FEE_ACCOUNTS.some((it) => [transferData.info.destination, transferData.info.destinationOwner].includes(it))) {
transferData.isFee = true;
}
if (actions[groupKey]) {
actions[groupKey].push(transferData);
}
else {
actions[groupKey] = [transferData];
}
}
});
});
// process transfers without program
groupKey = 'transfer';
this.adapter.instructions?.forEach((ix, outerIndex) => {
const transferData = this.parseInstructionAction(ix, `${outerIndex}`, extraTypes);
if (transferData) {
if (actions[groupKey]) {
actions[groupKey].push(transferData);
}
else {
actions[groupKey] = [transferData];
}
}
});
return actions;
}
processTransferInstructions(outerIndex, extraTypes) {
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 !== null));
}
/**
* Parse instruction actions (both parsed and compiled)
* actions: transfer/transferCheced/mintTo/burn
*/
parseInstructionAction(instruction, idx, extraTypes) {
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, idx, extraTypes) {
if ((0, transfer_utils_1.isTransfer)(instruction)) {
return (0, transfer_utils_1.processTransfer)(instruction, idx, this.adapter);
}
if ((0, transfer_utils_1.isNativeTransfer)(instruction)) {
return (0, transfer_utils_1.processNatvieTransfer)(instruction, idx, this.adapter);
}
if ((0, transfer_utils_1.isTransferCheck)(instruction)) {
return (0, transfer_utils_1.processTransferCheck)(instruction, idx, this.adapter);
}
if (extraTypes) {
const actions = extraTypes
.map((it) => {
if ((0, transfer_utils_1.isExtraAction)(instruction, it)) {
return (0, transfer_utils_1.processExtraAction)(instruction, idx, this.adapter, it);
}
})
.filter((it) => !!it);
return actions.length > 0 ? actions[0] : null;
}
return null;
}
/**
* Parse compiled instruction
*/
parseCompiledInstructionAction(instruction, idx, extraTypes) {
if ((0, transfer_compiled_utils_1.isCompiledTransfer)(instruction)) {
return (0, transfer_compiled_utils_1.processCompiledTransfer)(instruction, idx, this.adapter);
}
if ((0, transfer_compiled_utils_1.isCompiledNativeTransfer)(instruction)) {
return (0, transfer_compiled_utils_1.processCompiledNatvieTransfer)(instruction, idx, this.adapter);
}
if ((0, transfer_compiled_utils_1.isCompiledTransferCheck)(instruction)) {
return (0, transfer_compiled_utils_1.processCompiledTransferCheck)(instruction, idx, this.adapter);
}
if (extraTypes) {
const actions = extraTypes
.map((it) => {
if ((0, transfer_compiled_utils_1.isCompiledExtraAction)(instruction, it)) {
return (0, transfer_compiled_utils_1.processCompiledExtraAction)(instruction, idx, this.adapter, it);
}
})
.filter((it) => !!it);
return actions.length > 0 ? actions[0] : null;
}
return null;
}
/**
* Get mint from instruction
*/
getMintFromInstruction(ix, info) {
let mint = this.adapter.splTokenMap.get(info.destination)?.mint;
if (!mint)
mint = this.adapter.splTokenMap.get(info.source)?.mint;
if (!mint && ix.programId === constants_1.TOKENS.NATIVE)
mint = constants_1.TOKENS.SOL;
return mint;
}
/**
* Get token amount from instruction info
*/
getTokenAmount(info, decimals) {
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) {
return constants_1.SKIP_PROGRAM_IDS.includes(programId) ||
Object.values(constants_1.DEX_PROGRAMS)
.filter((it) => it.tags.includes('vault'))
.map((it) => it.id)
.includes(programId);
}
/**
* Get transfer info from transfer data
*/
getTransferInfo(transferData, timestamp, signature) {
const { info } = transferData;
if (!info || !info.tokenAmount)
return null;
const 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) {
const timestamp = this.adapter.blockTime || 0;
const signature = this.adapter.signature;
return transferDataList
.map((data) => this.getTransferInfo(data, timestamp, signature))
.filter((info) => info !== null);
}
/**
* Process swap data from transfers
*/
processSwapData(transfers, dexInfo, skipNative = true) {
if (!transfers.length) {
throw new Error('No swap data provided');
}
const uniqueTokens = this.extractUniqueTokens(transfers, skipNative);
if (uniqueTokens.length < 2) {
return null;
// throw `Insufficient unique tokens for swap`;
}
const signer = this.getSwapSigner();
const { inputToken, outputToken, feeTransfer } = this.calculateTokenAmounts(signer, transfers, uniqueTokens);
const trade = {
type: (0, utils_1.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,
};
if (feeTransfer) {
trade.fee = {
mint: feeTransfer.info.mint,
amount: feeTransfer.info.tokenAmount.uiAmount,
amountRaw: feeTransfer.info.tokenAmount.amount,
decimals: feeTransfer.info.tokenAmount.decimals,
};
}
return trade;
}
/**
* Get signer for swap transaction
*/
getSwapSigner() {
const defaultSigner = this.adapter.accountKeys[0];
// Check for Jupiter DCA program
const isDCAProgram = this.adapter.accountKeys.find((key) => key === constants_1.DEX_PROGRAMS.JUPITER_DCA.id);
return isDCAProgram ? this.adapter.accountKeys[2] : defaultSigner;
}
/**
* Extract unique tokens from transfers
*/
extractUniqueTokens(transfers, skipNative) {
const uniqueTokens = [];
const seenTokens = new Set();
transfers.forEach((transfer) => {
if (skipNative && transfer.info.mint == (constants_1.TOKENS.NATIVE)) {
return; // Skip native SOL transfers in most case (fee/tip/createAccount)
}
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
*/
calculateTokenAmounts(signer, transfers, uniqueTokens) {
let inputToken = uniqueTokens[0];
let outputToken = uniqueTokens[uniqueTokens.length - 1];
if (outputToken.source == signer || outputToken.authority == signer) {
[inputToken, outputToken] = [outputToken, inputToken];
}
const { inputAmount, inputAmountRaw, outputAmount, outputAmountRaw, feeTransfer } = this.sumTokenAmounts(transfers, inputToken.mint, outputToken.mint);
return {
inputToken: {
...inputToken,
amount: inputAmount,
amountRaw: inputAmountRaw.toString(),
},
outputToken: {
...outputToken,
amount: outputAmount,
amountRaw: outputAmountRaw.toString(),
},
feeTransfer,
};
}
/**
* Sum token amounts from transfers
*/
sumTokenAmounts(transfers, inputMint, outputMint) {
const seenTransfers = new Set();
let inputAmount = 0;
let outputAmount = 0;
let inputAmountRaw = 0n;
let outputAmountRaw = 0n;
let feeTransfer;
transfers.forEach((transfer) => {
const tokenInfo = this.getTransferTokenInfo(transfer);
if (!tokenInfo)
return;
const destination = tokenInfo.destinationOwner || tokenInfo.destination || '';
if (constants_1.FEE_ACCOUNTS.includes(destination)) {
feeTransfer = transfer;
return; // skip fee transfer
}
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, feeTransfer };
}
/**
* Get token info from transfer data
*/
getTransferTokenInfo(transfer) {
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;
}
attachTradeFee(trade) {
if (trade) {
if (!trade.fee) {
const mint = trade.outputToken.mint;
const token = mint == constants_1.TOKENS.SOL
? this.adapter.getAccountSolBalanceChanges(true).get(trade.user)
: this.adapter.getAccountTokenBalanceChanges(true).get(trade.user)?.get(mint);
if (token) {
const feeAmount = BigInt(trade.outputToken.amountRaw) - BigInt(token.change.amount);
if (feeAmount > 0n) {
const feeUiAmount = (0, types_1.convertToUiAmount)(feeAmount, trade.outputToken.decimals);
// add fee
trade.fee = {
mint,
amount: feeUiAmount,
amountRaw: feeAmount.toString(),
decimals: trade.outputToken.decimals,
};
// update outAmount
trade.outputToken.balanceChange = token.change.amount;
}
}
}
if (trade.inputToken.mint == constants_1.TOKENS.SOL) {
const token = this.adapter.getAccountSolBalanceChanges(true).get(trade.user);
if (token) {
if (Math.abs(token.change.uiAmount || 0) > trade.inputToken.amount) {
trade.inputToken.balanceChange = token.change.amount;
}
}
}
}
return trade;
}
/**
* Process transfer data for meme token events
* Handles the common transfer processing logic
* @param cInst Classified instruction
* @param event Meme event to update
* @param baseMint Base token mint address
* @param skipNative Whether to skip native SOL transfers
* @param transferStartIdx Starting index for transfer processing
* @returns Updated meme event or null if processing fails
*/
processMemeTransferData(cInst, event, baseMint, skipNative, transferStartIdx, transferActions) {
const transfers = this.getTransfersForInstruction(transferActions, cInst.programId, cInst.outerIndex, cInst.innerIndex);
if (transfers.length < 2) {
return event;
}
const dexInfo = {
programId: cInst.programId,
amm: this.getDexProgramName(cInst.programId),
route: '', // Will be set by getDexInfo if needed
};
const trade = this.processSwapData(transfers.slice(transferStartIdx), dexInfo, skipNative);
if (!trade) {
return event;
}
// Validate trade direction and token mints
if (event.type === 'BUY') {
if (trade.outputToken.mint !== baseMint) {
throw new Error(`baseMint mismatch: expected ${baseMint}, got ${trade.outputToken.mint}`);
}
}
else if (event.type === 'SELL') {
if (trade.inputToken.mint !== baseMint) {
throw new Error(`baseMint mismatch: expected ${baseMint}, got ${trade.inputToken.mint}`);
}
}
this.updateMemeTokenInfo(event, trade);
return event;
}
/**
* Get DEX program name by program ID
* @param programId Program ID
* @returns Program name or 'Unknown'
*/
getDexProgramName(programId) {
const dexProgram = Object.values(constants_1.DEX_PROGRAMS).find((dex) => dex.id === programId);
return dexProgram?.name || 'Unknown';
}
/**
* Update meme event token information from trade data
* @param event Meme event to update
* @param trade Trade information source
*/
updateMemeTokenInfo(event, trade) {
// Initialize token info if not exists
if (!event.inputToken) {
event.inputToken = {
mint: '',
amount: 0,
amountRaw: '0',
decimals: 0
};
}
if (!event.outputToken) {
event.outputToken = {
mint: '',
amount: 0,
amountRaw: '0',
decimals: 0
};
}
// Update input token info
event.inputToken.mint = trade.inputToken.mint;
event.inputToken.amount = trade.inputToken.amount;
event.inputToken.amountRaw = trade.inputToken.amountRaw;
event.inputToken.decimals = trade.inputToken.decimals;
// Update output token info
event.outputToken.mint = trade.outputToken.mint;
event.outputToken.amount = trade.outputToken.amount;
event.outputToken.amountRaw = trade.outputToken.amountRaw;
event.outputToken.decimals = trade.outputToken.decimals;
// Update fee info if available
if (trade.fee) {
event.fee = trade.fee.amount;
}
else if (trade.fees && trade.fees.length > 0) {
// Sum all fees if multiple fees exist
let totalFee = 0;
for (const fee of trade.fees) {
totalFee += fee.amount;
}
event.fee = totalFee;
}
}
/**
* Filter transfers for a specific instruction
* @param transferActions Map of transfer actions
* @param programId Program ID
* @param outerIndex Outer instruction index
* @param innerIndex Inner instruction index (optional)
* @param filterTypes Types to filter by
* @returns Filtered transfer data array
*/
filterTransfersForInstruction(transferActions, programId, outerIndex, innerIndex, filterTypes) {
// Create the key to look up in the transferActions map
let key = `${programId}:${outerIndex}`;
if (innerIndex !== undefined) {
key += `-${innerIndex}`;
}
// Get transfers for the instruction
const transfers = transferActions[key];
if (!transfers) {
return [];
}
// If no filter types specified, return all transfers
if (!filterTypes || filterTypes.length === 0) {
return transfers;
}
// Filter transfers by type
const result = [];
for (const transfer of transfers) {
for (const filterType of filterTypes) {
if (transfer.type === filterType) {
result.push(transfer);
break;
}
}
}
return result;
}
/**
* Get transfers for a specific instruction with default types
* Default types: transfer, transferChecked
* @param transferActions Map of transfer actions
* @param programId Program ID
* @param outerIndex Outer instruction index
* @param innerIndex Inner instruction index (optional)
* @param extraTypes Additional types to include
* @returns Transfer data array
*/
getTransfersForInstruction(transferActions, programId, outerIndex, innerIndex, extraTypes) {
const defaultTypes = ['transfer', 'transferChecked'];
if (extraTypes) {
defaultTypes.push(...extraTypes);
}
return this.filterTransfersForInstruction(transferActions, programId, outerIndex, innerIndex, defaultTypes);
}
}
exports.TransactionUtils = TransactionUtils;
//# sourceMappingURL=transaction-utils.js.map