2026-02-09 14:46:19 +08:00
|
|
|
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) {
|
2026-05-28 10:23:56 +08:00
|
|
|
requiredAccounts := 12
|
|
|
|
|
if bytes.Equal(instruction.Data[:8], meteoraDammV2InitializePoolWithDynamicConfig[:]) {
|
|
|
|
|
requiredAccounts = 13
|
|
|
|
|
} else if bytes.Equal(instruction.Data[:8], meteoraDammV2InitializeCustomizablePoolDiscriminator[:]) {
|
|
|
|
|
requiredAccounts = 11
|
|
|
|
|
}
|
|
|
|
|
if len(instruction.Accounts) < requiredAccounts {
|
2026-02-09 14:46:19 +08:00
|
|
|
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 {
|
2026-02-11 14:58:12 +08:00
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("meta damm initial get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1])
|
2026-02-09 14:46:19 +08:00
|
|
|
}
|
|
|
|
|
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]
|
2026-02-11 14:58:12 +08:00
|
|
|
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]
|
|
|
|
|
}
|
2026-02-09 14:46:19 +08:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-16 14:24:14 +08:00
|
|
|
func meteoraDammSwapAmountInfo(event string, params *struct {
|
|
|
|
|
Amount0 uint64
|
|
|
|
|
Amount1 uint64
|
|
|
|
|
SwapMode uint8
|
|
|
|
|
}) (swapMode SwapMode, fixedAmount decimal.Decimal, limitAmount decimal.Decimal, ok bool) {
|
2026-04-20 12:31:30 +08:00
|
|
|
_ = event
|
2026-04-16 14:24:14 +08:00
|
|
|
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
|
|
|
|
|
//
|
2026-04-20 12:31:30 +08:00
|
|
|
// `SetSwapAmountInfo` derives sides from the normalized buy/sell event, so
|
|
|
|
|
// the instruction parameters should stay in raw IDL order here.
|
2026-04-16 14:24:14 +08:00
|
|
|
switch params.SwapMode {
|
|
|
|
|
case 0, 1: // ExactIn / PartialFill
|
|
|
|
|
swapMode = SwapModeExactIn
|
2026-04-20 12:31:30 +08:00
|
|
|
return swapMode, decimal.NewFromUint64(params.Amount0), decimal.NewFromUint64(params.Amount1), true
|
2026-04-16 14:24:14 +08:00
|
|
|
case 2: // ExactOut
|
|
|
|
|
swapMode = SwapModeExactOut
|
|
|
|
|
return swapMode, decimal.NewFromUint64(params.Amount0), decimal.NewFromUint64(params.Amount1), true
|
|
|
|
|
default:
|
|
|
|
|
return SwapModeUnknown, decimal.Zero, decimal.Zero, false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 14:46:19 +08:00
|
|
|
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")
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-16 14:24:14 +08:00
|
|
|
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
|
2026-02-09 14:46:19 +08:00
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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) {
|
2026-05-28 10:23:56 +08:00
|
|
|
if len(instruction.Accounts) < 9 {
|
2026-02-09 14:46:19 +08:00
|
|
|
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
|
|
|
|
|
}
|