package pump_parser import ( "bytes" "encoding/base64" "encoding/binary" "fmt" "strings" 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"` } type metaoraPoolSwapArgs struct { InAmount uint64 MinimumOutAmount uint64 } type metaoraPoolSwapEvent struct { InAmount uint64 OutAmount uint64 TradeFee uint64 ProtocolFee uint64 HostFee uint64 } 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") } var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] 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 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") } var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] 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 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) { swapOffset := offset var args metaoraPoolSwapArgs if err := agbinary.NewBorshDecoder(instruction.Data[8:]).Decode(&args); err != nil { return nil, increaseOffset(offset), fmt.Errorf("failed to decode meteora pools swap args: %w", err) } 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 { if args.InAmount == 0 { return nil, increaseOffset(offset), InstructionIgnoredError } 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, }, } fixedSide := fixedSwapAmountSide(event, SwapModeExactIn) limitSide := oppositeSwapAmountSide(fixedSide) if fixedSide == SwapAmountSideUnknown || limitSide == SwapAmountSideUnknown { swaps[0].SetSwapAmountInfo( SwapModeExactIn, decimal.NewFromUint64(args.InAmount), decimal.NewFromUint64(args.MinimumOutAmount), ) return swaps, offset, nil } actualLimitAmount := swapAmountForSide(baseAmount, quoteAmount, limitSide) if swapEvent, ok := metaoraPoolSwapEventFromInstruction(instruction); ok { actualLimitAmount = decimal.NewFromUint64(swapEvent.OutAmount) } else if swapEvent, ok := metaoraPoolSwapEventForOffset(tx, swapOffset); ok { actualLimitAmount = decimal.NewFromUint64(swapEvent.OutAmount) } swaps[0].SetSwapAmountInfoDetailed( SwapModeExactIn, decimal.NewFromUint64(args.InAmount), fixedSide, swapMintForSide(baseMint, quoteMint, fixedSide), SwapLimitTypeMinOut, decimal.NewFromUint64(args.MinimumOutAmount), limitSide, swapMintForSide(baseMint, quoteMint, limitSide), actualLimitAmount, ) return swaps, offset, nil } func metaoraPoolSwapEventFromInstruction(instruction Instruction) (metaoraPoolSwapEvent, bool) { for _, event := range instruction.LogEvents { if swapEvent, ok := metaoraPoolDecodeSwapEventData(event); ok { return swapEvent, true } } return metaoraPoolSwapEvent{}, false } func metaoraPoolSwapEventForOffset(tx *Tx, offset [2]uint) (metaoraPoolSwapEvent, bool) { if tx == nil || tx.rawTx == nil { return metaoraPoolSwapEvent{}, false } occurrence := metaoraPoolSwapInstructionOccurrence(tx.rawTx, offset) if occurrence == 0 { return metaoraPoolSwapEvent{}, false } return metaoraPoolSwapEventFromLogs(tx.rawTx.Meta.LogMessages, occurrence) } func metaoraPoolSwapInstructionOccurrence(rawTx *RawTx, offset [2]uint) int { if rawTx == nil { return 0 } accountList := rawTx.getAccountList() innerByOuter := make(map[int]InnerInstructions, len(rawTx.Meta.InnerInstructions)) for _, inner := range rawTx.Meta.InnerInstructions { innerByOuter[inner.Index] = inner } occurrence := 0 for i, instruction := range rawTx.Transaction.Message.Instructions { if uint(i) == offset[0] && offset[1] == 0 { if metaoraPoolIsSwapInstruction(accountList, instruction) { return occurrence + 1 } return 0 } if metaoraPoolIsSwapInstruction(accountList, instruction) { occurrence++ } inner := innerByOuter[i] for j, instruction := range inner.Instructions { innerOffset := uint(j + 1) if uint(i) == offset[0] && offset[1] == innerOffset { if metaoraPoolIsSwapInstruction(accountList, instruction) { return occurrence + 1 } return 0 } if metaoraPoolIsSwapInstruction(accountList, instruction) { occurrence++ } } } return 0 } func metaoraPoolIsSwapInstruction(accountList []solana.PublicKey, instruction Instruction) bool { if instruction.ProgramIDIndex < 0 || instruction.ProgramIDIndex >= len(accountList) { return false } if !accountList[instruction.ProgramIDIndex].Equals(metaoraPoolProgramID) { return false } return len(instruction.Data) >= 8 && bytes.Equal(instruction.Data[:8], metaoraPoolSwapDiscriminator[:]) } func metaoraPoolSwapEventFromLogs(logMessages []string, occurrence int) (metaoraPoolSwapEvent, bool) { if occurrence <= 0 { return metaoraPoolSwapEvent{}, false } type frame struct { program string sawSwap bool } targetProgram := metaoraPoolProgramID.String() var stack []frame seen := 0 for _, logMessage := range logMessages { if program, ok := metaoraPoolLogInvokeProgram(logMessage); ok { stack = append(stack, frame{program: program}) continue } if len(stack) > 0 && stack[len(stack)-1].program == targetProgram { if logMessage == "Program log: Instruction: Swap" { stack[len(stack)-1].sawSwap = true continue } if stack[len(stack)-1].sawSwap && strings.HasPrefix(logMessage, "Program data: ") { event, ok := metaoraPoolDecodeSwapEventLog(strings.TrimSpace(strings.TrimPrefix(logMessage, "Program data: "))) if ok { seen++ if seen == occurrence { return event, true } } } } if program, ok := metaoraPoolLogFinishedProgram(logMessage); ok { for i := len(stack) - 1; i >= 0; i-- { if stack[i].program == program { stack = stack[:i] break } } } } return metaoraPoolSwapEvent{}, false } func metaoraPoolLogInvokeProgram(logMessage string) (string, bool) { if !strings.HasPrefix(logMessage, "Program ") || !strings.Contains(logMessage, " invoke [") { return "", false } rest := strings.TrimPrefix(logMessage, "Program ") program, _, ok := strings.Cut(rest, " ") return program, ok } func metaoraPoolLogFinishedProgram(logMessage string) (string, bool) { if !strings.HasPrefix(logMessage, "Program ") { return "", false } if !strings.HasSuffix(logMessage, " success") && !strings.Contains(logMessage, " failed:") { return "", false } rest := strings.TrimPrefix(logMessage, "Program ") program, _, ok := strings.Cut(rest, " ") return program, ok } func metaoraPoolDecodeSwapEventLog(encoded string) (metaoraPoolSwapEvent, bool) { data, err := base64.StdEncoding.DecodeString(encoded) if err != nil { return metaoraPoolSwapEvent{}, false } return metaoraPoolDecodeSwapEventData(data) } func metaoraPoolDecodeSwapEventData(data []byte) (metaoraPoolSwapEvent, bool) { if len(data) < 48 { return metaoraPoolSwapEvent{}, false } if !bytes.Equal(data[:8], metaoraPoolSwapEventDiscriminator[:]) { return metaoraPoolSwapEvent{}, false } body := data[8:] return metaoraPoolSwapEvent{ InAmount: binary.LittleEndian.Uint64(body[0:8]), OutAmount: binary.LittleEndian.Uint64(body[8:16]), TradeFee: binary.LittleEndian.Uint64(body[16:24]), ProtocolFee: binary.LittleEndian.Uint64(body[24:32]), HostFee: binary.LittleEndian.Uint64(body[32:40]), }, true }