Files
pump-parser/metaorapool.go

879 lines
33 KiB
Go
Raw Permalink Normal View History

2026-02-09 14:46:19 +08:00
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")
}
2026-02-10 10:32:46 +08:00
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
2026-02-09 14:46:19 +08:00
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")
}
2026-02-10 10:32:46 +08:00
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
2026-02-09 14:46:19 +08:00
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) {
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
}