package pump_parser import ( "bytes" "encoding/binary" "fmt" agbinary "github.com/gagliardetto/binary" "github.com/gagliardetto/solana-go" "github.com/shopspring/decimal" ) type metaoraPoolInitializePoolData struct { TokenAAmount uint64 `json:"tokenAAmount"` TokenBAmount uint64 `json:"tokenBAmount"` } var ( meteoraVaultProgram = solana.MustPublicKeyFromBase58("24Uqj9JCLxUeoC3hGfh5W3s9FM9uCHDS2SG3LYwBpyTi") meteoraVaultDepositDiscriminator = []byte{0xf2, 0x23, 0xc6, 0x89, 0x52, 0xe1, 0xf2, 0xb6} meteoraVaultWithdrawDiscriminator = []byte{0xb7, 0x12, 0x46, 0x9c, 0x94, 0x6d, 0xa1, 0x22} tokenProgramMintToDiscriminator = []byte{0x07} tokenProgramTransferDiscriminator = []byte{0x03} tokenProgramBurnDiscriminator = []byte{0x08} ) func metaoraPoolParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { if !tx.rawTx.accountList[instruction.ProgramIDIndex].Equals(metaoraPoolProgramID) { return nil, increaseOffset(offset), fmt.Errorf("metaoraPool program instruction not found, offset, %d, %d", offset[0], offset[1]) } decode := instruction.Data if len(decode) < 8 { return nil, increaseOffset(offset), fmt.Errorf("metaoraPool program instruction data too short, offset, %d, %d", offset[0], offset[1]) } discriminator := *(*[8]byte)(decode[:8]) switch discriminator { case metaoraPoolInitializePermissionlessConstantProductPoolWithConfigDiscriminator, metaoraPoolInitializePermissionlessConstantProductPoolWithConfig2Discriminator: return metaoraPoolInitializePermissionlessConstantProductPoolWithConfig(tx, instruction, innerInstructions, offset) case metaoraPoolInitializePermissionlessPoolDiscriminator, metaoraPoolInitializePermissionlessPoolWithFeeTierDiscriminator, metaoraPoolInitializeCustomizablePermissionlessConstantProductPoolDiscriminator: return metaoraPoolInitializePermissionlessPool(tx, instruction, innerInstructions, offset) case metaoraPoolInitializePermissionedPoolDiscriminator: return metaoraPoolInitializePermissionedPool(tx, instruction, innerInstructions, offset) case metaoraPoolSwapDiscriminator: return metaoraPoolSwap(tx, instruction, innerInstructions, offset) case metaoraPoolAddImbalanceLiquidityDiscriminator, metaoraPoolAddBalanceLiquidityDiscriminator, metaoraPoolBootstrapLiquidityDiscriminator: return metaoraPoolAddLiquidity(tx, instruction, innerInstructions, offset) case metaoraPoolRemoveLiquiditySingleSideDiscriminator, metaoraPoolRemoveBalanceLiquidityDiscriminator, metaoraPoolClaimFeeDiscriminator: return metaoraPoolRemoveLiquidity(tx, instruction, innerInstructions, offset) default: return nil, increaseOffset(offset), InstructionIgnoredError } } // InitializePermissionlessConstantProductPoolWithConfig // InitializePermissionlessConstantProductPoolWithConfig2 func metaoraPoolInitializePermissionlessConstantProductPoolWithConfig(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { var data metaoraPoolInitializePoolData err := agbinary.NewBorshDecoder(instruction.Data[8:]).Decode(&data) if err != nil { return nil, increaseOffset(offset), fmt.Errorf("failed to deserialize initialize pool data: %w", err) } if len(instruction.Accounts) < 20 { return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for initialize instruction") } tokenAMint := tx.rawTx.accountList[instruction.Accounts[3]] tokenBMint := tx.rawTx.accountList[instruction.Accounts[4]] baseVaultAccountIndex := instruction.Accounts[7] quoteVaultAccountIndex := instruction.Accounts[8] baseVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultAccountIndex) quoteVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultAccountIndex) if baseVaultTokenBalance == nil || quoteVaultTokenBalance == nil { return nil, increaseOffset(offset), fmt.Errorf("failed to get token balances") } baseReserve, err := decimal.NewFromString(baseVaultTokenBalance.UITokenAmount.Amount) if err != nil { return nil, increaseOffset(offset), fmt.Errorf("failed to parse base reserve: %w", err) } quoteReserve, err := decimal.NewFromString(quoteVaultTokenBalance.UITokenAmount.Amount) if err != nil { return nil, increaseOffset(offset), fmt.Errorf("failed to parse quote reserve: %w", err) } 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 pools initial get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1]) } var meteoraVaultProgramId int for i, acc := range tx.rawTx.accountList { if acc.Equals(meteoraVaultProgram) { meteoraVaultProgramId = i break } } var baseFound, quoteFound bool if meteoraVaultProgramId > 0 { for innerIndex, innerInstr := range inners { if innerInstr.ProgramIDIndex == meteoraVaultProgramId && len(innerInstr.Data) >= 16 && bytes.Equal(innerInstr.Data[:8], pumpEventDiscriminator[:]) && bytes.Equal(innerInstr.Data[8:16], meteoraVaultDepositDiscriminator[:]) { if len(innerInstr.Accounts) < 2 { continue } if !baseFound && innerInstr.Accounts[1] == baseVaultAccountIndex { baseFound = true } if !quoteFound && innerInstr.Accounts[1] == quoteVaultAccountIndex { quoteFound = true } if baseFound && quoteFound { if offset[1] == 0 { offset[0] += 1 } else { offset[1] = uint(innerIndex) + 1 + prefixLen } break } } } } return []Swap{ { Program: SolProgramMeteoraPools, Event: "create", Pool: tx.rawTx.accountList[instruction.Accounts[0]], BaseMint: tokenAMint, QuoteMint: tokenBMint, BaseTokenProgram: baseVaultTokenBalance.ProgramIDAccount, QuoteTokenProgram: quoteVaultTokenBalance.ProgramIDAccount, Creator: tx.rawTx.accountList[0], BaseMintDecimals: uint8(baseVaultTokenBalance.UITokenAmount.Decimals), QuoteMintDecimals: uint8(quoteVaultTokenBalance.UITokenAmount.Decimals), User: tx.rawTx.accountList[instruction.Accounts[18]], BaseAmount: decimal.NewFromUint64(data.TokenAAmount), QuoteAmount: decimal.NewFromUint64(data.TokenBAmount), BaseReserve: baseReserve, QuoteReserve: quoteReserve, EntryContract: entryContract, }, }, offset, nil } // InitializePermissionlessPool // InitializePermissionlessPoolWithFeeTier func metaoraPoolInitializePermissionlessPool(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { // discriminator + tokenA amount + tokenB amount if len(instruction.Data) < 24 { return nil, increaseOffset(offset), fmt.Errorf("not enough data for initialize instruction") } var data metaoraPoolInitializePoolData err := agbinary.NewBorshDecoder(instruction.Data[len(instruction.Data)-16:]).Decode(&data) if err != nil { return nil, increaseOffset(offset), fmt.Errorf("failed to deserialize initialize pool data: %w", err) } if len(instruction.Accounts) < 20 { return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for initialize instruction") } tokenAMint := tx.rawTx.accountList[instruction.Accounts[2]] tokenBMint := tx.rawTx.accountList[instruction.Accounts[3]] baseVaultAccountIndex := instruction.Accounts[6] quoteVaultAccountIndex := instruction.Accounts[7] baseVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultAccountIndex) quoteVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultAccountIndex) if baseVaultTokenBalance == nil || quoteVaultTokenBalance == nil { return nil, increaseOffset(offset), fmt.Errorf("failed to get token balances") } baseReserve, err := decimal.NewFromString(baseVaultTokenBalance.UITokenAmount.Amount) if err != nil { return nil, increaseOffset(offset), fmt.Errorf("failed to parse base reserve: %w", err) } quoteReserve, err := decimal.NewFromString(quoteVaultTokenBalance.UITokenAmount.Amount) if err != nil { return nil, increaseOffset(offset), fmt.Errorf("failed to parse quote reserve: %w", err) } 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 pools initial get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1]) } var meteoraVaultProgramId int for i, acc := range tx.rawTx.accountList { if acc.Equals(meteoraVaultProgram) { meteoraVaultProgramId = i break } } var baseFound, quoteFound bool if meteoraVaultProgramId > 0 { for innerIndex, innerInstr := range inners { if innerInstr.ProgramIDIndex == meteoraVaultProgramId && len(innerInstr.Data) >= 16 && bytes.Equal(innerInstr.Data[:8], pumpEventDiscriminator[:]) && bytes.Equal(innerInstr.Data[8:16], meteoraVaultDepositDiscriminator[:]) { if len(innerInstr.Accounts) < 2 { continue } if !baseFound && innerInstr.Accounts[1] == baseVaultAccountIndex { baseFound = true } if !quoteFound && innerInstr.Accounts[1] == quoteVaultAccountIndex { quoteFound = true } if baseFound && quoteFound { if offset[1] == 0 { offset[0] += 1 } else { offset[1] = uint(innerIndex) + 1 + prefixLen } break } } } } return []Swap{ { Program: SolProgramMeteoraPools, Event: "create", Pool: tx.rawTx.accountList[instruction.Accounts[0]], BaseMint: tokenAMint, QuoteMint: tokenBMint, BaseTokenProgram: baseVaultTokenBalance.ProgramIDAccount, QuoteTokenProgram: quoteVaultTokenBalance.ProgramIDAccount, Creator: tx.rawTx.accountList[0], BaseMintDecimals: uint8(baseVaultTokenBalance.UITokenAmount.Decimals), QuoteMintDecimals: uint8(quoteVaultTokenBalance.UITokenAmount.Decimals), User: tx.rawTx.accountList[instruction.Accounts[18]], BaseAmount: decimal.NewFromUint64(data.TokenAAmount), QuoteAmount: decimal.NewFromUint64(data.TokenBAmount), BaseReserve: baseReserve, QuoteReserve: quoteReserve, EntryContract: entryContract, }, }, offset, nil } func metaoraPoolInitializePermissionedPool(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { // discriminator + tokenA amount + tokenB amount if len(instruction.Data) < 24 { return nil, increaseOffset(offset), fmt.Errorf("not enough data for initialize instruction") } var data metaoraPoolInitializePoolData err := agbinary.NewBorshDecoder(instruction.Data[len(instruction.Data)-16:]).Decode(&data) if err != nil { return nil, increaseOffset(offset), fmt.Errorf("failed to deserialize initialize pool data: %w", err) } if len(instruction.Accounts) < 20 { return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for initialize instruction") } tokenAMint := tx.rawTx.accountList[instruction.Accounts[2]] tokenBMint := tx.rawTx.accountList[instruction.Accounts[3]] baseVaultAccountIndex := instruction.Accounts[10] quoteVaultAccountIndex := instruction.Accounts[11] baseVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultAccountIndex) quoteVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultAccountIndex) if baseVaultTokenBalance == nil || quoteVaultTokenBalance == nil { return nil, increaseOffset(offset), fmt.Errorf("failed to get token balances") } baseReserve, err := decimal.NewFromString(baseVaultTokenBalance.UITokenAmount.Amount) if err != nil { return nil, increaseOffset(offset), fmt.Errorf("failed to parse base reserve: %w", err) } quoteReserve, err := decimal.NewFromString(quoteVaultTokenBalance.UITokenAmount.Amount) if err != nil { return nil, increaseOffset(offset), fmt.Errorf("failed to parse quote reserve: %w", err) } 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 pools initial get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1]) } var meteoraVaultProgramId int for i, acc := range tx.rawTx.accountList { if acc.Equals(meteoraVaultProgram) { meteoraVaultProgramId = i break } } var baseFound, quoteFound bool if meteoraVaultProgramId > 0 { for innerIndex, innerInstr := range inners { if innerInstr.ProgramIDIndex == meteoraVaultProgramId && len(innerInstr.Data) >= 16 && bytes.Equal(innerInstr.Data[:8], pumpEventDiscriminator[:]) && bytes.Equal(innerInstr.Data[8:16], meteoraVaultDepositDiscriminator[:]) { if len(innerInstr.Accounts) < 2 { continue } if !baseFound && innerInstr.Accounts[1] == baseVaultAccountIndex { baseFound = true } if !quoteFound && innerInstr.Accounts[1] == quoteVaultAccountIndex { quoteFound = true } if baseFound && quoteFound { if offset[1] == 0 { offset[0] += 1 } else { offset[1] = uint(innerIndex) + 1 + prefixLen } break } } } } return []Swap{ { Program: SolProgramMeteoraPools, Event: "create", Pool: tx.rawTx.accountList[instruction.Accounts[0]], BaseMint: tokenAMint, QuoteMint: tokenBMint, BaseTokenProgram: baseVaultTokenBalance.ProgramIDAccount, QuoteTokenProgram: quoteVaultTokenBalance.ProgramIDAccount, Creator: tx.rawTx.accountList[0], BaseMintDecimals: uint8(baseVaultTokenBalance.UITokenAmount.Decimals), QuoteMintDecimals: uint8(quoteVaultTokenBalance.UITokenAmount.Decimals), User: tx.rawTx.accountList[instruction.Accounts[18]], BaseAmount: decimal.NewFromUint64(data.TokenAAmount), QuoteAmount: decimal.NewFromUint64(data.TokenBAmount), BaseReserve: baseReserve, QuoteReserve: quoteReserve, EntryContract: entryContract, }, }, offset, nil } // BootstrapLiquidity // AddImbalanceLiquidity // AddBalanceLiquidity func metaoraPoolAddLiquidity(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { if len(instruction.Accounts) < 14 { return nil, increaseOffset(offset), fmt.Errorf("invalid instruction accounts length") } pool := tx.rawTx.accountList[instruction.Accounts[0]] lpMint := tx.rawTx.accountList[instruction.Accounts[1]] payer := tx.rawTx.accountList[instruction.Accounts[13]] userPoolLp := tx.rawTx.accountList[instruction.Accounts[2]] // vault for storing real tokens // NOTE: because meteora pools will put assets of different pairs together, // we cannot directly use the vault balance to calculate liquidity var meteoraVaultProgramId int for i, acc := range tx.rawTx.accountList { if acc.Equals(meteoraVaultProgram) { meteoraVaultProgramId = i break } } if meteoraVaultProgramId == 0 { return nil, increaseOffset(offset), fmt.Errorf("meteora vault program not found") } // 7, 8 baseVaultLpAccountBalance, _ := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[7]) quoteVaultLpAccountBalance, _ := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[8]) if baseVaultLpAccountBalance == nil || quoteVaultLpAccountBalance == nil { return nil, increaseOffset(offset), InstructionIgnoredError //fmt.Errorf("failed to get vault lp account balances") } // 9,10 baseVaultAccountBalance, _ := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[9]) quoteVaultAccountBalance, _ := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[10]) if baseVaultAccountBalance == nil || quoteVaultAccountBalance == nil { return nil, increaseOffset(offset), fmt.Errorf("failed to get vault lp account balances") } var prefixLen = offset[1] inners, err := getInnerInstructions(innerInstructions, prefixLen) if err != nil { return nil, increaseOffset(offset), fmt.Errorf("meta pools initial get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1]) } var baseFound, quoteFound bool var baseAmount, quoteAmount decimal.Decimal var ( baseMint = solana.PublicKey{} quoteMint = solana.PublicKey{} baseTokenProgram = solana.PublicKey{} quoteTokenProgram = solana.PublicKey{} baseDecimals uint8 quoteDecimals uint8 baseReserve decimal.Decimal quoteReserve decimal.Decimal ) baseMint = baseVaultAccountBalance.MintAccount quoteMint = quoteVaultAccountBalance.MintAccount quoteTokenProgram = quoteVaultAccountBalance.ProgramIDAccount baseTokenProgram = baseVaultAccountBalance.ProgramIDAccount baseDecimals = uint8(baseVaultAccountBalance.UITokenAmount.Decimals) baseReserve, err = decimal.NewFromString(baseVaultLpAccountBalance.UITokenAmount.Amount) if err != nil { return nil, increaseOffset(offset), fmt.Errorf("failed to parse base reserve: %w", err) } if baseDecimals != uint8(baseVaultLpAccountBalance.UITokenAmount.Decimals) { decimalDiff := int(baseDecimals) - int(baseVaultLpAccountBalance.UITokenAmount.Decimals) baseReserve = baseReserve.Mul(decimal.NewFromInt(10).Pow(decimal.NewFromInt(int64(decimalDiff)))) } quoteDecimals = uint8(quoteVaultAccountBalance.UITokenAmount.Decimals) quoteReserve, err = decimal.NewFromString(quoteVaultLpAccountBalance.UITokenAmount.Amount) if err != nil { return nil, increaseOffset(offset), fmt.Errorf("failed to parse quote reserve: %w", err) } if quoteDecimals != uint8(quoteVaultLpAccountBalance.UITokenAmount.Decimals) { decimalDiff := int(quoteDecimals) - int(quoteVaultLpAccountBalance.UITokenAmount.Decimals) quoteReserve = quoteReserve.Mul(decimal.NewFromInt(10).Pow(decimal.NewFromInt(int64(decimalDiff)))) } for innerIndex := 0; innerIndex < len(inners); innerIndex++ { innerInstr := inners[innerIndex] if innerInstr.ProgramIDIndex == meteoraVaultProgramId && len(innerInstr.Data) >= 16 && bytes.Equal(innerInstr.Data[:8], eventDiscriminator[:]) && bytes.Equal(innerInstr.Data[8:16], meteoraVaultDepositDiscriminator[:]) { if len(innerInstr.Accounts) < 6 { continue } if innerIndex+1 >= len(inners) { continue } transferInstr := inners[innerIndex+1] _, to, amount, err := parseTokenTransfer(tx.rawTx, transferInstr) if err != nil { continue } innerIndex++ // skip transfer instruction if !baseFound && to.Equals(tx.rawTx.accountList[baseVaultAccountBalance.AccountIndex]) { baseFound = true baseAmount = decimal.NewFromUint64(amount) } else if !quoteFound && to.Equals(tx.rawTx.accountList[quoteVaultAccountBalance.AccountIndex]) { quoteFound = true quoteAmount = decimal.NewFromUint64(amount) } } if (baseFound || quoteFound) && (tx.rawTx.accountList[innerInstr.ProgramIDIndex] == solana.TokenProgramID || tx.rawTx.accountList[innerInstr.ProgramIDIndex] == solana.Token2022ProgramID) && innerInstr.Data[0] == 7 { if len(innerInstr.Accounts) < 3 { continue } // mint lp token if tx.rawTx.accountList[innerInstr.Accounts[0]] == lpMint && tx.rawTx.accountList[innerInstr.Accounts[1]] == userPoolLp { if offset[1] == 0 { offset[0] += 1 } else { offset[1] = uint(innerIndex) + 1 + prefixLen } break } } } if !baseFound && !quoteFound { return nil, increaseOffset(offset), fmt.Errorf("failed to find deposit instructions") } var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] var event = "add_liquidity_one_side" if baseFound && quoteFound { // both sides event = "add_liquidity" } swap := Swap{ Program: SolProgramMeteoraPools, Event: event, Pool: pool, BaseMint: baseMint, QuoteMint: quoteMint, BaseTokenProgram: baseTokenProgram, QuoteTokenProgram: quoteTokenProgram, BaseMintDecimals: baseDecimals, QuoteMintDecimals: quoteDecimals, User: payer, BaseAmount: baseAmount, QuoteAmount: quoteAmount, BaseReserve: baseReserve, QuoteReserve: quoteReserve, EntryContract: entryContract, } return []Swap{swap}, offset, nil } // RemoveLiquiditySingleSide // ClaimFee // RemoveBalanceLiquidity func metaoraPoolRemoveLiquidity(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { if len(instruction.Accounts) < 14 { return nil, increaseOffset(offset), fmt.Errorf("invalid instruction accounts length") } pool := tx.rawTx.accountList[instruction.Accounts[0]] lpMint := tx.rawTx.accountList[instruction.Accounts[1]] var ( userPoolLp solana.PublicKey baseVaultIdx int quoteVaultIdx int baseLpVaultIdx int quoteLpVaultIdx int userIdx int ) if bytes.Equal(instruction.Data[:8], metaoraPoolRemoveLiquiditySingleSideDiscriminator[:]) { userPoolLp = tx.rawTx.accountList[instruction.Accounts[2]] //userBaseAccountIdx = 11 //userQuoteAccountIdx = 12 baseVaultIdx = 9 quoteVaultIdx = 10 baseLpVaultIdx = 3 quoteLpVaultIdx = 4 userIdx = 12 } else if bytes.Equal(instruction.Data[:8], metaoraPoolClaimFeeDiscriminator[:]) { if len(instruction.Accounts) < 16 { return nil, increaseOffset(offset), fmt.Errorf("invalid instruction accounts length") } userPoolLp = tx.rawTx.accountList[instruction.Accounts[5]] //userBaseAccountIdx = 15 //userQuoteAccountIdx = 16 baseVaultIdx = 7 quoteVaultIdx = 8 baseLpVaultIdx = 11 quoteLpVaultIdx = 12 userIdx = 3 } else if bytes.Equal(instruction.Data[:8], metaoraPoolRemoveBalanceLiquidityDiscriminator[:]) { userPoolLp = tx.rawTx.accountList[instruction.Accounts[2]] //userBaseAccountIdx = 11 //userQuoteAccountIdx = 12 baseVaultIdx = 9 quoteVaultIdx = 10 baseLpVaultIdx = 3 quoteLpVaultIdx = 4 userIdx = 12 } else { return nil, increaseOffset(offset), fmt.Errorf("invalid remove liquidity instruction discriminator") } // vault for storing real tokens // NOTE: because meteora pools will put assets of different pairs together, // we cannot directly use the vault balance to calculate liquidity var meteoraVaultProgramId int for i, acc := range tx.rawTx.accountList { if acc.Equals(meteoraVaultProgram) { meteoraVaultProgramId = i break } } if meteoraVaultProgramId == 0 { return nil, increaseOffset(offset), fmt.Errorf("meteora vault program not found") } // 7, 8 baseVaultLpAccountBalance, _ := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[baseLpVaultIdx]) quoteVaultLpAccountBalance, _ := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[quoteLpVaultIdx]) if baseVaultLpAccountBalance == nil || quoteVaultLpAccountBalance == nil { return nil, increaseOffset(offset), InstructionIgnoredError // fmt.Errorf("failed to get vault lp account balances") } // 9,10 baseVaultAccountBalance, _ := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[baseVaultIdx]) quoteVaultAccountBalance, _ := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[quoteVaultIdx]) if baseVaultAccountBalance == nil || quoteVaultAccountBalance == nil { return nil, increaseOffset(offset), fmt.Errorf("failed to get vault lp account balances") } var prefixLen = offset[1] inners, err := getInnerInstructions(innerInstructions, prefixLen) if err != nil { return nil, increaseOffset(offset), fmt.Errorf("meta pools initial get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1]) } var baseFound, quoteFound bool var baseAmount, quoteAmount decimal.Decimal var ( baseMint = solana.PublicKey{} quoteMint = solana.PublicKey{} baseTokenProgram = solana.PublicKey{} quoteTokenProgram = solana.PublicKey{} baseDecimals uint8 quoteDecimals uint8 baseReserve decimal.Decimal quoteReserve decimal.Decimal ) baseMint = baseVaultAccountBalance.MintAccount quoteMint = quoteVaultAccountBalance.MintAccount baseTokenProgram = baseVaultAccountBalance.ProgramIDAccount quoteTokenProgram = quoteVaultAccountBalance.ProgramIDAccount baseDecimals = uint8(baseVaultAccountBalance.UITokenAmount.Decimals) baseReserve, err = decimal.NewFromString(baseVaultLpAccountBalance.UITokenAmount.Amount) if err != nil { return nil, increaseOffset(offset), fmt.Errorf("failed to parse base reserve: %w", err) } if baseDecimals != uint8(baseVaultLpAccountBalance.UITokenAmount.Decimals) { decimalDiff := int(baseDecimals) - int(baseVaultLpAccountBalance.UITokenAmount.Decimals) baseReserve = baseReserve.Mul(decimal.NewFromInt(10).Pow(decimal.NewFromInt(int64(decimalDiff)))) } quoteDecimals = uint8(quoteVaultAccountBalance.UITokenAmount.Decimals) quoteReserve, err = decimal.NewFromString(quoteVaultLpAccountBalance.UITokenAmount.Amount) if err != nil { return nil, increaseOffset(offset), fmt.Errorf("failed to parse quote reserve: %w", err) } if quoteDecimals != uint8(quoteVaultLpAccountBalance.UITokenAmount.Decimals) { decimalDiff := int(quoteDecimals) - int(quoteVaultLpAccountBalance.UITokenAmount.Decimals) quoteReserve = quoteReserve.Mul(decimal.NewFromInt(10).Pow(decimal.NewFromInt(int64(decimalDiff)))) } for innerIndex := 0; innerIndex < len(inners); innerIndex++ { innerInstr := inners[innerIndex] if innerInstr.ProgramIDIndex == meteoraVaultProgramId && len(innerInstr.Data) >= 8 && bytes.Equal(innerInstr.Data[:8], meteoraVaultWithdrawDiscriminator[:]) { if len(innerInstr.Accounts) < 6 { continue } if innerIndex+1 >= len(inners) { continue } transferInstr := inners[innerIndex+1] from, _, amount, err := parseTokenTransfer(tx.rawTx, transferInstr) if err != nil { fmt.Println("parse tx error:", err, tx.GetTxHash(), transferInstr) continue } innerIndex++ // skip transfer instruction if !baseFound && from.Equals(tx.rawTx.accountList[instruction.Accounts[baseVaultIdx]]) { //base baseFound = true baseAmount = decimal.NewFromUint64(amount) } else if !quoteFound && from.Equals(tx.rawTx.accountList[instruction.Accounts[quoteVaultIdx]]) { // quote quoteFound = true quoteAmount = decimal.NewFromUint64(amount) } } if (baseFound || quoteFound) && (tx.rawTx.accountList[innerInstr.ProgramIDIndex] == solana.TokenProgramID || tx.rawTx.accountList[innerInstr.ProgramIDIndex] == solana.Token2022ProgramID) && innerInstr.Data[0] == 8 { if len(innerInstr.Accounts) < 3 { continue } // mint lp token if tx.rawTx.accountList[innerInstr.Accounts[1]] == lpMint && tx.rawTx.accountList[innerInstr.Accounts[0]] == userPoolLp { if offset[1] == 0 { offset[0] += 1 } else { offset[1] = uint(innerIndex) + 1 + prefixLen } break } } } if !baseFound && !quoteFound { return nil, increaseOffset(offset), fmt.Errorf("failed to find withdraw instructions, baseFound: %v, quoteFound: %v", baseFound, quoteFound) } var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] var event = "remove_liquidity_one_side" if baseFound && quoteFound { event = "remove_liquidity" } swap := Swap{ Program: SolProgramMeteoraPools, Event: event, Pool: pool, BaseMint: baseMint, QuoteMint: quoteMint, BaseTokenProgram: baseTokenProgram, QuoteTokenProgram: quoteTokenProgram, BaseMintDecimals: baseDecimals, QuoteMintDecimals: quoteDecimals, User: tx.rawTx.accountList[userIdx], BaseAmount: baseAmount, QuoteAmount: quoteAmount, BaseReserve: baseReserve, QuoteReserve: quoteReserve, EntryContract: entryContract, } return []Swap{swap}, offset, nil } func metaoraPoolSwap(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { pool := tx.rawTx.accountList[instruction.Accounts[0]] payer := tx.rawTx.accountList[instruction.Accounts[12]] sourceAccountIndex := instruction.Accounts[1] destinationAccountIndex := instruction.Accounts[2] // vault for storing real tokens // NOTE: because meteora pools will put assets of different pairs together, // we cannot directly use the vault balance to calculate liquidity //parse reserves from vault accounts baseVaultIdx := instruction.Accounts[6] quoteVaultIdx := instruction.Accounts[5] baseVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultIdx) quoteVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultIdx) if baseVaultTokenBalance == nil || quoteVaultTokenBalance == nil { return nil, increaseOffset(offset), fmt.Errorf("failed to get vault token balances") } baseVaultLpBalance, err := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[10]) quoteVaultLpBalance, err := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[9]) if baseVaultLpBalance == nil || quoteVaultLpBalance == nil { return nil, increaseOffset(offset), fmt.Errorf("failed to get vault lp balances") } baseTokenProgram := baseVaultTokenBalance.ProgramIDAccount quoteTokenProgram := quoteVaultTokenBalance.ProgramIDAccount baseMint := baseVaultTokenBalance.MintAccount quoteMint := quoteVaultTokenBalance.MintAccount baseDecimals := uint8(baseVaultTokenBalance.UITokenAmount.Decimals) quoteDecimals := uint8(quoteVaultTokenBalance.UITokenAmount.Decimals) baseReserve := decimal.Zero quoteReserve := decimal.Zero if baseVaultLpBalance.UITokenAmount.Decimals == baseVaultTokenBalance.UITokenAmount.Decimals { baseReserve, _ = decimal.NewFromString(baseVaultLpBalance.UITokenAmount.Amount) } else { decimalsDiff := int32(baseVaultTokenBalance.UITokenAmount.Decimals) - int32(baseVaultLpBalance.UITokenAmount.Decimals) multiplier := decimal.NewFromInt(10).Pow(decimal.NewFromInt32(decimalsDiff)) baseLpAmount, _ := decimal.NewFromString(baseVaultLpBalance.UITokenAmount.Amount) baseReserve = baseLpAmount.Mul(multiplier) } if quoteVaultLpBalance.UITokenAmount.Decimals == quoteVaultTokenBalance.UITokenAmount.Decimals { quoteReserve, _ = decimal.NewFromString(quoteVaultLpBalance.UITokenAmount.Amount) } else { decimalsDiff := int32(quoteVaultTokenBalance.UITokenAmount.Decimals) - int32(quoteVaultLpBalance.UITokenAmount.Decimals) multiplier := decimal.NewFromInt(10).Pow(decimal.NewFromInt32(decimalsDiff)) quoteLpAmount, _ := decimal.NewFromString(quoteVaultLpBalance.UITokenAmount.Amount) quoteReserve = quoteLpAmount.Mul(multiplier) } 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 pools initial get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1]) } var meteoraVaultProgramId int for i, acc := range tx.rawTx.accountList { if acc.Equals(meteoraVaultProgram) { meteoraVaultProgramId = i break } } var baseFound, quoteFound bool var ( baseAmount decimal.Decimal quoteAmount decimal.Decimal event string ) for innerIndex := 0; innerIndex < len(inners); innerIndex++ { innerInstr := inners[innerIndex] // if innerInstr.ProgramIDIndex == meteoraVaultProgramId && len(innerInstr.Data) >= 8 && (bytes.Equal(innerInstr.Data[:8], meteoraVaultWithdrawDiscriminator[:]) || bytes.Equal(innerInstr.Data[:8], meteoraVaultDepositDiscriminator[:])) { if len(innerInstr.Accounts) < 6 { continue } if innerIndex+1 >= len(inners) { continue } transferInstr := inners[innerIndex+1] if (tx.rawTx.accountList[transferInstr.ProgramIDIndex] != solana.TokenProgramID && tx.rawTx.accountList[transferInstr.ProgramIDIndex] != solana.Token2022ProgramID) || transferInstr.Data[0] != 3 { continue } innerIndex++ // skip transfer instruction if len(innerInstr.Accounts) == 7 && (bytes.Equal(innerInstr.Data[:8], meteoraVaultWithdrawDiscriminator[:]) || bytes.Equal(innerInstr.Data[:8], meteoraVaultDepositDiscriminator[:])) { if innerInstr.Accounts[1] == baseVaultIdx { //base baseFound = true baseAmount = decimal.NewFromUint64(binary.LittleEndian.Uint64(transferInstr.Data[1:9])) if bytes.Equal(innerInstr.Data[:8], meteoraVaultWithdrawDiscriminator[:]) { event = "buy" } else { event = "sell" } } else if innerInstr.Accounts[1] == quoteVaultIdx { // quote quoteFound = true quoteAmount = decimal.NewFromUint64(binary.LittleEndian.Uint64(transferInstr.Data[1:9])) } } } if baseFound && quoteFound { if offset[1] == 0 { offset[0] += 1 } else { offset[1] = uint(innerIndex) + 1 + prefixLen + 1 // +1 for mint or withdraw instruction, } break } } if !baseFound || !quoteFound { return nil, increaseOffset(offset), fmt.Errorf("failed to find meteora pool event in inner instructions") } userBase := getAccountBalanceAfterTx(tx.rawTx, sourceAccountIndex) userQuote := getAccountBalanceAfterTx(tx.rawTx, destinationAccountIndex) swaps := []Swap{ { Program: SolProgramMeteoraPools, Event: event, Pool: pool, BaseMint: baseMint, QuoteMint: quoteMint, BaseTokenProgram: baseTokenProgram, QuoteTokenProgram: quoteTokenProgram, Creator: solana.PublicKey{}, BaseMintDecimals: baseDecimals, QuoteMintDecimals: quoteDecimals, User: payer, BaseAmount: baseAmount, QuoteAmount: quoteAmount, BaseReserve: baseReserve, QuoteReserve: quoteReserve, UserBaseBalance: userBase, UserQuoteBalance: userQuote, EntryContract: entryContract, }, } return swaps, offset, nil }