solana-dex-parser
Version:
Solana Dex Transaction Parser
140 lines (118 loc) • 5.23 kB
text/typescript
import { TokenAmount } from '@solana/web3.js';
import { DEX_PROGRAMS, DISCRIMINATORS, TOKENS } from '../../constants';
import { convertToUiAmount, TradeInfo, TradeType } from '../../types';
import { absBigInt, getInstructionData } from '../../utils';
import { BaseParser } from '../base-parser';
export class MoonshotParser extends BaseParser {
public processTrades(): TradeInfo[] {
const trades: TradeInfo[] = [];
this.classifiedInstructions.forEach(({ instruction, programId, outerIndex, innerIndex }) => {
if (this.isTradeInstruction(instruction, programId)) {
const trade = this.parseTradeInstruction(instruction, `${outerIndex}-${innerIndex ?? 0}`);
if (trade) {
trades.push(trade);
}
}
});
return trades;
}
private isTradeInstruction(instruction: any, programId: string): boolean {
const accounts = this.adapter.getInstructionAccounts(instruction);
return programId === DEX_PROGRAMS.MOONSHOT.id && accounts && accounts.length === 11;
}
private parseTradeInstruction(instruction: any, idx: string): TradeInfo | null {
try {
if (!('data' in instruction)) return null;
const data = getInstructionData(instruction);
const discriminator = data.slice(0, 8);
let tradeType: TradeType;
if (discriminator.equals(DISCRIMINATORS.MOONSHOT.BUY)) {
tradeType = 'BUY';
} else if (discriminator.equals(DISCRIMINATORS.MOONSHOT.SELL)) {
tradeType = 'SELL';
} else {
return null;
}
const moonshotTokenMint = this.adapter.getInstructionAccounts(instruction)[6];
const accountKeys = this.adapter.accountKeys;
const collateralMint = this.detectCollateralMint(accountKeys);
const { tokenAmount, collateralAmount } = this.calculateAmounts(moonshotTokenMint, collateralMint);
const trade: TradeInfo = {
type: tradeType,
inputToken: {
mint: tradeType === 'BUY' ? collateralMint : moonshotTokenMint,
amount: tradeType === 'BUY' ? (collateralAmount.uiAmount ?? 0) : (tokenAmount.uiAmount ?? 0),
amountRaw: tradeType === 'BUY' ? collateralAmount.amount : tokenAmount.amount,
decimals: tradeType === 'BUY' ? collateralAmount.decimals : tokenAmount.decimals,
},
outputToken: {
mint: tradeType === 'BUY' ? moonshotTokenMint : collateralMint,
amount: tradeType === 'BUY' ? (tokenAmount.uiAmount ?? 0) : (collateralAmount.uiAmount ?? 0),
amountRaw: tradeType === 'BUY' ? tokenAmount.amount : collateralAmount.amount,
decimals: tradeType === 'BUY' ? tokenAmount.decimals : collateralAmount.decimals,
},
user: this.adapter.signer,
programId: DEX_PROGRAMS.MOONSHOT.id,
amm: DEX_PROGRAMS.MOONSHOT.name,
route: this.dexInfo.route || '',
slot: this.adapter.slot,
timestamp: this.adapter.blockTime,
signature: this.adapter.signature,
idx,
};
return this.utils.attachTokenTransferInfo(trade, this.transferActions);
} catch (error) {
console.error('Failed to parse Moonshot trade:', error);
return null;
}
}
private detectCollateralMint(accountKeys: string[]): string {
if (accountKeys.some((key) => key === TOKENS.USDC)) return TOKENS.USDC;
if (accountKeys.some((key) => key === TOKENS.USDT)) return TOKENS.USDT;
return TOKENS.SOL;
}
private calculateAmounts(tokenMint: string, collateralMint: string) {
const tokenBalanceChanges = this.getTokenBalanceChanges(tokenMint);
const collateralBalanceChanges = this.getTokenBalanceChanges(collateralMint);
return {
tokenAmount: this.createTokenAmount(absBigInt(tokenBalanceChanges), tokenMint),
collateralAmount: this.createTokenAmount(absBigInt(collateralBalanceChanges), collateralMint),
};
}
private getTokenBalanceChanges(mint: string): bigint {
const signer = this.adapter.signer;
if (mint === TOKENS.SOL) {
if (!this.adapter.postBalances?.[0] || !this.adapter.preBalances?.[0]) {
throw new Error('Insufficient balance information for SOL');
}
return BigInt(this.adapter.postBalances[0] - this.adapter.preBalances[0]);
}
let preAmount = BigInt(0);
let postAmount = BigInt(0);
let balanceFound = false;
this.adapter.preTokenBalances?.forEach((preBalance) => {
if (preBalance.mint === mint && preBalance.owner === signer) {
preAmount = BigInt(preBalance.uiTokenAmount.amount);
balanceFound = true;
}
});
this.adapter.postTokenBalances?.forEach((postBalance) => {
if (postBalance.mint === mint && postBalance.owner === signer) {
postAmount = BigInt(postBalance.uiTokenAmount.amount);
balanceFound = true;
}
});
if (!balanceFound) {
throw new Error('Could not find balance for specified mint and signer');
}
return postAmount - preAmount;
}
private createTokenAmount(amount: bigint, mint: string): TokenAmount {
const decimals = this.adapter.getTokenDecimals(mint);
return {
amount: amount.toString(),
uiAmount: convertToUiAmount(amount, decimals),
decimals,
};
}
}