package pump_parser import ( "bytes" "fmt" agbinary "github.com/gagliardetto/binary" "github.com/gagliardetto/solana-go" "github.com/shopspring/decimal" ) func metaoraDammParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { if !tx.rawTx.accountList[instruction.ProgramIDIndex].Equals(meteoraDammV2Program) { return nil, increaseOffset(offset), fmt.Errorf("metaora damm program instruction not found, offset, %d, %d", offset[0], offset[1]) } decode := instruction.Data if len(decode) < 8 { return nil, increaseOffset(offset), fmt.Errorf("metaora damm program instruction data too short, offset, %d, %d", offset[0], offset[1]) } discriminator := *(*[8]byte)(decode[:8]) switch discriminator { case meteoraDammV2InitializeCustomizablePoolDiscriminator, meteoraDammV2InitializePoolWithDynamicConfig, meteoraDammV2InitializePoolDiscriminator: return meteoraDammV2InitializePoolParser(tx, instruction, innerInstructions, offset) case meteoraDammV2SwapDiscriminator, meteoraDammV2SwapV2Discriminator: return meteoraDammV2Swap(tx, instruction, innerInstructions, offset) case meteoraDammV2AddLiquidityDiscriminator: return meteoraDammV2AddLiquidityParser(tx, instruction, innerInstructions, offset) case meteoraDammV2RemoveLiquidityDiscriminator, meteoraDammV2RemoveAllLiquidityDiscriminator: return meteoraDammV2RemoveLiquidityParser(tx, instruction, innerInstructions, offset) default: return nil, increaseOffset(offset), InstructionIgnoredError } } var ( metaoraDammInitializePoolDiscriminator = []byte{228, 69, 165, 46, 81, 203, 154, 29, 228, 50, 246, 85, 203, 66, 134, 37} meteoraDammSwapDiscriminator = []byte{228, 69, 165, 46, 81, 203, 154, 29, 189, 66, 51, 168, 38, 80, 117, 153} // EvtLiquidityChange meteoraDammAddLiquidityDiscriminator = []byte{228, 69, 165, 46, 81, 203, 154, 29, 197, 171, 78, 127, 224, 211, 87, 13} meteoraDammRemoveLiquidityDiscriminator = []byte{228, 69, 165, 46, 81, 203, 154, 29, 197, 171, 78, 127, 224, 211, 87, 13} ) type MetaoraDammDynamicFeeParameters struct { BinStep uint16 BinStepU128 [16]byte FilterPeriod uint16 DecayPeriod uint16 ReductionFactor uint16 MaxVolatilityAccumulator uint32 VariableFeeControl uint32 } type MetaoraDammInitializePoolEvent struct { Pool solana.PublicKey `json:"pool"` TokenAMint solana.PublicKey `json:"tokenAMint"` TokenBMint solana.PublicKey `json:"tokenBMint"` Creator solana.PublicKey `json:"creator"` Payer solana.PublicKey `json:"payer"` AlphaVault solana.PublicKey `json:"alphaVault"` //PoolFees *struct { // BaseFee [30]byte // DynamicFee *MetaoraDammDynamicFeeParameters `json:"dynamicFee"` //} `json:"poolFees"` //SqrtMinPrice [16]byte `json:"sqrtMinPrice"` //SqrtMaxPrice [16]byte `json:"sqrtMaxPrice"` //ActivationType uint8 `json:"activationType"` //CollectFeeMode uint8 `json:"collectFeeMode"` //Liquidity [16]byte `json:"liquidity"` //SqrtPrice [16]byte `json:"sqrtPrice"` //ActivationPoint uint64 `json:"activationPoint"` //TokenAFlag uint8 `json:"tokenAFlag"` //TokenBFlag uint8 `json:"tokenBFlag"` //TokenAAmount uint64 `json:"tokenAAmount"` //TokenBAmount uint64 `json:"tokenBAmount"` //TotalAmountA uint64 `json:"totalAmountA"` //TotalAmountB uint64 `json:"totalAmountB"` //PoolType uint8 `json:"poolType"` } func meteoraDammV2InitializePoolParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { if len(instruction.Accounts) < 12 { return nil, increaseOffset(offset), fmt.Errorf("invalid instruction accounts length") } var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] var prefixLen = offset[1] inners, err := getInnerInstructions(innerInstructions, prefixLen) if err != nil { return nil, increaseOffset(offset), fmt.Errorf("meta damm initial get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1]) } var loadedEvent bool var initializePoolEvent MetaoraDammInitializePoolEvent for i, innerInstruction := range inners { if innerInstruction.ProgramIDIndex == instruction.ProgramIDIndex && len(innerInstruction.Data) >= 16 && bytes.Equal(innerInstruction.Data[:16], metaoraDammInitializePoolDiscriminator) { err := agbinary.NewBorshDecoder(innerInstruction.Data[16:]).Decode(&initializePoolEvent) if err != nil { if offset[1] == 0 { offset[0] += 1 } else { offset[1] = uint(i) + 1 + prefixLen } return nil, offset, fmt.Errorf("failed to deserialize initialize pool event: %w", err) } if offset[1] == 0 { offset[0] += 1 } else { offset[1] = uint(i) + 1 + prefixLen } loadedEvent = true break } } if !loadedEvent { return nil, increaseOffset(offset), fmt.Errorf("failed to get initialize pool event") } baseVaultAccountIndex := instruction.Accounts[10] quoteVaultAccountIndex := instruction.Accounts[11] if bytes.Equal(instruction.Data[:8], meteoraDammV2InitializePoolWithDynamicConfig[:]) { baseVaultAccountIndex = instruction.Accounts[11] quoteVaultAccountIndex = instruction.Accounts[12] } else if bytes.Equal(instruction.Data[:8], meteoraDammV2InitializeCustomizablePoolDiscriminator[:]) { baseVaultAccountIndex = instruction.Accounts[9] quoteVaultAccountIndex = instruction.Accounts[10] } baseVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultAccountIndex) if err != nil { return nil, increaseOffset(offset), fmt.Errorf("metaora damm get base vault token balance error: %v, offset, %d, %d", err, offset[0], offset[1]) } quoteVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultAccountIndex) if err != nil { return nil, increaseOffset(offset), fmt.Errorf("metaora damm get quote vault token balance error: %v, offset, %d, %d", err, offset[0], offset[1]) } baseReserve, _ := decimal.NewFromString(baseVaultTokenBalance.UITokenAmount.Amount) quoteReserve, _ := decimal.NewFromString(quoteVaultTokenBalance.UITokenAmount.Amount) swap := Swap{ Program: SolProgramMeteoraAmmV2, Event: "create", Pool: initializePoolEvent.Pool, BaseMint: initializePoolEvent.TokenAMint, QuoteMint: initializePoolEvent.TokenBMint, BaseTokenProgram: baseVaultTokenBalance.ProgramIDAccount, QuoteTokenProgram: quoteVaultTokenBalance.ProgramIDAccount, Creator: initializePoolEvent.Creator, BaseMintDecimals: uint8(baseVaultTokenBalance.UITokenAmount.Decimals), QuoteMintDecimals: uint8(quoteVaultTokenBalance.UITokenAmount.Decimals), BaseReserve: baseReserve, QuoteReserve: quoteReserve, User: tx.rawTx.accountList[instruction.Accounts[0]], LpMint: tx.rawTx.accountList[instruction.Accounts[1]], EntryContract: entryContract, } return []Swap{swap}, offset, nil } type meteoraDammSwapEvent struct { Pool solana.PublicKey TradeDirection uint8 CollectFeeMode uint8 HasReferral bool Params *struct { Amount0 uint64 Amount1 uint64 SwapMode uint8 } SwapResult *struct { IncludedFeeInputAmount uint64 ExcludedFeeInputAmount uint64 AmountLeft uint64 OutputAmount uint64 NextSqrtPrice [16]byte TradingFee uint64 ProtocolFee uint64 PartnerFee uint64 ReferralFee uint64 } IncludedTransferFeeAmountIn uint64 IncludedTransferFeeAmountOut uint64 ExcludedTransferFeeAmountOut uint64 CurrentTimestamp uint64 ReserveAAmount uint64 ReserveBAmount uint64 } func meteoraDammSwapAmountInfo(event string, params *struct { Amount0 uint64 Amount1 uint64 SwapMode uint8 }) (swapMode SwapMode, fixedAmount decimal.Decimal, limitAmount decimal.Decimal, ok bool) { _ = event if params == nil { return SwapModeUnknown, decimal.Zero, decimal.Zero, false } // Meteora DAMM v2 IDL defines: // - swap: SwapParameters{ amountIn, minimumAmountOut } // - swap2: SwapParameters2{ amount0, amount1, swapMode } // - ExactIn / PartialFill: amount0=amount_in, amount1=minimum_amount_out // - ExactOut: amount0=amount_out, amount1=maximum_amount_in // // `SetSwapAmountInfo` derives sides from the normalized buy/sell event, so // the instruction parameters should stay in raw IDL order here. switch params.SwapMode { case 0, 1: // ExactIn / PartialFill swapMode = SwapModeExactIn return swapMode, decimal.NewFromUint64(params.Amount0), decimal.NewFromUint64(params.Amount1), true case 2: // ExactOut swapMode = SwapModeExactOut return swapMode, decimal.NewFromUint64(params.Amount0), decimal.NewFromUint64(params.Amount1), true default: return SwapModeUnknown, decimal.Zero, decimal.Zero, false } } func meteoraDammV2Swap(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { if len(instruction.Accounts) < 9 { return nil, increaseOffset(offset), fmt.Errorf("invalid instruction accounts length") } sourceAccountIndex := instruction.Accounts[2] destinationAccountIndex := instruction.Accounts[3] baseVaultAccountIndex := instruction.Accounts[4] quoteVaultAccountIndex := instruction.Accounts[5] tokenAMint := tx.rawTx.accountList[instruction.Accounts[6]] tokenBMint := tx.rawTx.accountList[instruction.Accounts[7]] payer := tx.rawTx.accountList[instruction.Accounts[8]] baseVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultAccountIndex) if err != nil { return nil, increaseOffset(offset), fmt.Errorf("metaora damm get base vault token balance error: %v, offset, %d, %d", err, offset[0], offset[1]) } quoteVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultAccountIndex) if err != nil { return nil, increaseOffset(offset), fmt.Errorf("metaora damm get quote vault token balance error: %v, offset, %d, %d", err, offset[0], offset[1]) } baseReserve, _ := decimal.NewFromString(baseVaultTokenBalance.UITokenAmount.Amount) quoteReserve, _ := decimal.NewFromString(quoteVaultTokenBalance.UITokenAmount.Amount) baseMint := tokenAMint quoteMint := tokenBMint baseTokenProgram := baseVaultTokenBalance.ProgramIDAccount quoteTokenProgram := quoteVaultTokenBalance.ProgramIDAccount baseMintDecimals := uint8(baseVaultTokenBalance.UITokenAmount.Decimals) quoteMintDecimals := uint8(quoteVaultTokenBalance.UITokenAmount.Decimals) userInputTokenBalance := getAccountBalanceAfterTx(tx.rawTx, sourceAccountIndex) userOutputTokenBalance := getAccountBalanceAfterTx(tx.rawTx, destinationAccountIndex) var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] var prefixLen = offset[1] inners, err := getInnerInstructions(innerInstructions, prefixLen) if err != nil { return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initial get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1]) } var loadedEvent bool var swapEvent meteoraDammSwapEvent for i, innerInstruction := range inners { if innerInstruction.ProgramIDIndex == instruction.ProgramIDIndex && len(innerInstruction.Data) >= 16 && bytes.Equal(innerInstruction.Data[:16], meteoraDammSwapDiscriminator) { err := agbinary.NewBorshDecoder(innerInstruction.Data[16:]).Decode(&swapEvent) if offset[1] == 0 { offset[0] += 1 } else { offset[1] = uint(i) + 1 + prefixLen } if err != nil { return nil, offset, fmt.Errorf("failed to deserialize swap event: %w", err) } loadedEvent = true break } } if !loadedEvent { return nil, increaseOffset(offset), fmt.Errorf("failed to get swap event") } var baseAmount decimal.Decimal var quoteAmount decimal.Decimal var userBase decimal.Decimal var userQuote decimal.Decimal event := "buy" if swapEvent.TradeDirection == 0 { // A -> B // sell base/A; buy quote/B event = "sell" userBase = userInputTokenBalance userQuote = userOutputTokenBalance baseAmount = decimal.NewFromUint64(swapEvent.SwapResult.IncludedFeeInputAmount) quoteAmount = decimal.NewFromUint64(swapEvent.ExcludedTransferFeeAmountOut) } else if swapEvent.TradeDirection == 1 { // B -> A // sell quote/B; buy base/A userBase = userOutputTokenBalance userQuote = userInputTokenBalance baseAmount = decimal.NewFromUint64(swapEvent.ExcludedTransferFeeAmountOut) quoteAmount = decimal.NewFromUint64(swapEvent.SwapResult.IncludedFeeInputAmount) } else { return nil, offset, fmt.Errorf("invalid trade direction") } swap := Swap{ Program: SolProgramMeteoraAmmV2, Event: event, Pool: swapEvent.Pool, BaseMint: baseMint, QuoteMint: quoteMint, BaseTokenProgram: baseTokenProgram, QuoteTokenProgram: quoteTokenProgram, Creator: solana.PublicKey{}, BaseMintDecimals: baseMintDecimals, QuoteMintDecimals: quoteMintDecimals, User: payer, BaseAmount: baseAmount, QuoteAmount: quoteAmount, BaseReserve: baseReserve, QuoteReserve: quoteReserve, UserBaseBalance: userBase, UserQuoteBalance: userQuote, EntryContract: entryContract, } if swapMode, fixedAmount, limitAmount, ok := meteoraDammSwapAmountInfo(event, swapEvent.Params); ok { swap.SetSwapAmountInfo(swapMode, fixedAmount, limitAmount) } return []Swap{swap}, offset, nil } type MeteoraDammV2LiquidityData struct { LiquidityDelta [16]byte `json:"liquidityDelta"` TokenAAmounthreshold uint64 `json:"tokenAAmounthreshold"` TokenBAmounthreshold uint64 `json:"tokenBAmounthreshold"` } type MeteoraDammV2AddLiquidityEvent struct { Pool solana.PublicKey `json:"pool"` Position solana.PublicKey `json:"position"` Owner solana.PublicKey `json:"owner"` Params *MeteoraDammV2LiquidityData `json:"params"` TokenAAmount uint64 `json:"tokenAAmount"` TokenBAmount uint64 `json:"tokenBAmount"` TotalAmountA uint64 `json:"totalAmountA"` TotalAmountB uint64 `json:"totalAmountB"` } type MeteoraDammV2RemoveLiquidityEvent struct { Pool solana.PublicKey `json:"pool"` Position solana.PublicKey `json:"position"` Owner solana.PublicKey `json:"owner"` Params *MeteoraDammV2LiquidityData `json:"params"` TokenAAmount uint64 `json:"tokenAAmount"` TokenBAmount uint64 `json:"tokenBAmount"` } func meteoraDammV2AddLiquidityParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { if len(instruction.Accounts) < 8 { return nil, increaseOffset(offset), fmt.Errorf("invalid instruction accounts length") } tokenAMint := tx.rawTx.accountList[instruction.Accounts[6]] tokenBMint := tx.rawTx.accountList[instruction.Accounts[7]] baseVaultAccountIndex := instruction.Accounts[4] quoteVaultAccountIndex := instruction.Accounts[5] baseVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultAccountIndex) if err != nil { return nil, increaseOffset(offset), fmt.Errorf("metaora damm get base vault token balance error: %v, offset, %d, %d", err, offset[0], offset[1]) } quoteVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultAccountIndex) if err != nil { return nil, increaseOffset(offset), fmt.Errorf("metaora damm get quote vault token balance error: %v, offset, %d, %d", err, offset[0], offset[1]) } baseReserve, _ := decimal.NewFromString(baseVaultTokenBalance.UITokenAmount.Amount) quoteReserve, _ := decimal.NewFromString(quoteVaultTokenBalance.UITokenAmount.Amount) baseTokenProgram := baseVaultTokenBalance.ProgramIDAccount quoteTokenProgram := quoteVaultTokenBalance.ProgramIDAccount baseMintDecimals := uint8(baseVaultTokenBalance.UITokenAmount.Decimals) quoteMintDecimals := uint8(quoteVaultTokenBalance.UITokenAmount.Decimals) var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] var prefixLen = offset[1] inners, err := getInnerInstructions(innerInstructions, prefixLen) if err != nil { return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initial get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1]) } var loadedEvent bool var liquidityEvent MeteoraDammV2AddLiquidityEvent for i, innerInstruction := range inners { if innerInstruction.ProgramIDIndex == instruction.ProgramIDIndex && len(innerInstruction.Data) >= 16 && bytes.Equal(innerInstruction.Data[:16], meteoraDammAddLiquidityDiscriminator[:]) { err := agbinary.NewBorshDecoder(innerInstruction.Data[16:]).Decode(&liquidityEvent) if err != nil { if offset[1] == 0 { offset[0] += 1 } else { offset[1] = uint(i) + 1 + prefixLen } return nil, offset, fmt.Errorf("failed to deserialize add liquidity event: %w", err) } if offset[1] == 0 { offset[0] += 1 } else { offset[1] = uint(i) + 1 + prefixLen } loadedEvent = true break } } if !loadedEvent { return nil, increaseOffset(offset), fmt.Errorf("failed to get add liquidity event") } swap := Swap{ Program: SolProgramMeteoraDLMM, Event: "add_liquidity", Pool: liquidityEvent.Pool, BaseMint: tokenAMint, QuoteMint: tokenBMint, BaseTokenProgram: baseTokenProgram, QuoteTokenProgram: quoteTokenProgram, BaseMintDecimals: baseMintDecimals, QuoteMintDecimals: quoteMintDecimals, User: liquidityEvent.Owner, BaseAmount: decimal.NewFromUint64(liquidityEvent.TokenAAmount), QuoteAmount: decimal.NewFromUint64(liquidityEvent.TokenBAmount), BaseReserve: baseReserve, QuoteReserve: quoteReserve, EntryContract: entryContract, } return []Swap{swap}, offset, nil } func meteoraDammV2RemoveLiquidityParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { if len(instruction.Accounts) < 8 { return nil, increaseOffset(offset), fmt.Errorf("invalid instruction accounts length") } tokenAMint := tx.rawTx.accountList[instruction.Accounts[7]] tokenBMint := tx.rawTx.accountList[instruction.Accounts[8]] baseVaultAccountIndex := instruction.Accounts[5] quoteVaultAccountIndex := instruction.Accounts[6] baseVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultAccountIndex) if err != nil { return nil, increaseOffset(offset), fmt.Errorf("metaora damm get base vault token balance error: %v, offset, %d, %d", err, offset[0], offset[1]) } quoteVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultAccountIndex) if err != nil { return nil, increaseOffset(offset), fmt.Errorf("metaora damm get quote vault token balance error: %v, offset, %d, %d", err, offset[0], offset[1]) } baseReserve, _ := decimal.NewFromString(baseVaultTokenBalance.UITokenAmount.Amount) quoteReserve, _ := decimal.NewFromString(quoteVaultTokenBalance.UITokenAmount.Amount) baseTokenProgram := baseVaultTokenBalance.ProgramIDAccount quoteTokenProgram := quoteVaultTokenBalance.ProgramIDAccount baseMintDecimals := uint8(baseVaultTokenBalance.UITokenAmount.Decimals) quoteMintDecimals := uint8(quoteVaultTokenBalance.UITokenAmount.Decimals) var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] var prefixLen = offset[1] inners, err := getInnerInstructions(innerInstructions, prefixLen) if err != nil { return nil, increaseOffset(offset), fmt.Errorf("meta damm get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1]) } var loadedEvent bool var liquidityEvent MeteoraDammV2RemoveLiquidityEvent for i, innerInstruction := range inners { if innerInstruction.ProgramIDIndex == instruction.ProgramIDIndex && len(innerInstruction.Data) >= 16 && bytes.Equal(innerInstruction.Data[:16], meteoraDammRemoveLiquidityDiscriminator[:]) { err := agbinary.NewBorshDecoder(innerInstruction.Data[16:]).Decode(&liquidityEvent) if err != nil { if offset[1] == 0 { offset[0] += 1 } else { offset[1] = uint(i) + 1 + prefixLen } return nil, offset, fmt.Errorf("failed to deserialize remove liquidity event: %w", err) } if offset[1] == 0 { offset[0] += 1 } else { offset[1] = uint(i) + 1 + prefixLen } loadedEvent = true break } } if !loadedEvent { return nil, increaseOffset(offset), fmt.Errorf("failed to get remove liquidity event") } swap := Swap{ Program: SolProgramMeteoraDLMM, Event: "remove_liquidity", Pool: liquidityEvent.Pool, BaseMint: tokenAMint, QuoteMint: tokenBMint, BaseTokenProgram: baseTokenProgram, QuoteTokenProgram: quoteTokenProgram, BaseMintDecimals: baseMintDecimals, QuoteMintDecimals: quoteMintDecimals, User: liquidityEvent.Owner, BaseAmount: decimal.NewFromUint64(liquidityEvent.TokenAAmount), QuoteAmount: decimal.NewFromUint64(liquidityEvent.TokenBAmount), BaseReserve: baseReserve, QuoteReserve: quoteReserve, EntryContract: entryContract, } return []Swap{swap}, offset, nil }