893 lines
34 KiB
Go
893 lines
34 KiB
Go
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"`
|
|
}
|
|
|
|
type metaoraPoolSwapArgs struct {
|
|
InAmount uint64
|
|
MinimumOutAmount 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) {
|
|
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 {
|
|
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,
|
|
},
|
|
}
|
|
swaps[0].SetSwapAmountInfo(
|
|
SwapModeExactIn,
|
|
decimal.NewFromUint64(args.InAmount),
|
|
decimal.NewFromUint64(args.MinimumOutAmount),
|
|
)
|
|
return swaps, offset, nil
|
|
}
|