2026-02-09 14:46:19 +08:00
|
|
|
package pump_parser
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
|
|
|
|
|
|
|
|
|
"github.com/shopspring/decimal"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func raydiumV4Parser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
|
|
|
|
if !tx.rawTx.accountList[instruction.ProgramIDIndex].Equals(raydiumV4Program) {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("raydiumv4 instruction not found, offset, %d, %d", offset[0], offset[1])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
decode := instruction.Data
|
|
|
|
|
if len(decode) < 1 {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("raydiumv4 program instruction data too short, offset, %d, %d", offset[0], offset[1])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
discriminator := decode[0]
|
|
|
|
|
|
|
|
|
|
switch discriminator {
|
|
|
|
|
case raydiumV4InitializePoolDiscriminator:
|
|
|
|
|
return raydiumv4InitializeParser(tx, instruction, innerInstructions, offset)
|
|
|
|
|
case raydiumV4AddLiquidityDiscriminator:
|
|
|
|
|
return raydiumv4AddLiquidityParser(tx, instruction, innerInstructions, offset)
|
|
|
|
|
case raydiumV4RemoveLiquidityDiscriminator:
|
|
|
|
|
return raydiumv4RemoveLiquidityParser(tx, instruction, innerInstructions, offset)
|
|
|
|
|
case raydiumV4WithdrawPNLDiscriminator:
|
|
|
|
|
return raydiumv4WithdrawPNLParser(tx, instruction, innerInstructions, offset)
|
|
|
|
|
case raydiumV4SwapBaseInDiscriminator, raydiumV4SwapBaseOutDiscriminator:
|
|
|
|
|
return raydiumv4SwapParser(tx, instruction, innerInstructions, offset)
|
2026-03-12 16:21:38 +08:00
|
|
|
case raydiumV4SwapBaseInV2Discriminator, raydiumV4SwapBaseOutV2Discriminator:
|
|
|
|
|
return raydiumv4SwapV2Parser(tx, instruction, innerInstructions, offset)
|
2026-02-09 14:46:19 +08:00
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
return nil, increaseOffset(offset), InstructionIgnoredError
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func raydiumv4InitializeParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
|
|
|
|
accountsLen := len(instruction.Accounts)
|
|
|
|
|
if accountsLen != 21 {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("invalid number of accounts for raydiumv4 initialize instruction, offset %d, %d", offset[0], offset[1])
|
|
|
|
|
}
|
|
|
|
|
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
|
|
|
|
//who := tx.rawTx.accountList[instruction.Accounts[accountsLen-1]]
|
|
|
|
|
user := tx.rawTx.accountList[instruction.Accounts[accountsLen-4]]
|
|
|
|
|
baseVaultIdx := instruction.Accounts[10]
|
|
|
|
|
quoteVaultIdx := instruction.Accounts[11]
|
|
|
|
|
|
|
|
|
|
baseTokenbalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultIdx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("failed to get base vault balance after tx: %v", err)
|
|
|
|
|
}
|
|
|
|
|
quoteTokenbalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultIdx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("failed to get quote vault balance after tx: %v", err)
|
|
|
|
|
}
|
|
|
|
|
baseReserve, _ := decimal.NewFromString(baseTokenbalance.UITokenAmount.Amount)
|
|
|
|
|
quoteReserve, _ := decimal.NewFromString(quoteTokenbalance.UITokenAmount.Amount)
|
|
|
|
|
offset[1] += 30
|
|
|
|
|
return []Swap{
|
|
|
|
|
{
|
|
|
|
|
Program: SolProgramRaydiumV4,
|
|
|
|
|
Event: "create",
|
|
|
|
|
Pool: tx.rawTx.accountList[instruction.Accounts[4]],
|
|
|
|
|
BaseMint: baseTokenbalance.MintAccount,
|
|
|
|
|
QuoteMint: quoteTokenbalance.MintAccount,
|
|
|
|
|
BaseTokenProgram: baseTokenbalance.ProgramIDAccount,
|
|
|
|
|
QuoteTokenProgram: quoteTokenbalance.ProgramIDAccount,
|
|
|
|
|
Creator: user,
|
|
|
|
|
BaseMintDecimals: uint8(baseTokenbalance.UITokenAmount.Decimals),
|
|
|
|
|
QuoteMintDecimals: uint8(quoteTokenbalance.UITokenAmount.Decimals),
|
|
|
|
|
User: user,
|
|
|
|
|
BaseReserve: baseReserve,
|
|
|
|
|
QuoteReserve: quoteReserve,
|
|
|
|
|
EntryContract: entryContract,
|
|
|
|
|
},
|
|
|
|
|
}, offset, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func raydiumv4AddLiquidityParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
|
|
|
|
accountsLen := len(instruction.Accounts)
|
|
|
|
|
if accountsLen != 14 && accountsLen != 15 {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("invalid number of accounts for raydiumv4 add liquidity instruction, offset %d, %d", offset[0], offset[1])
|
|
|
|
|
}
|
|
|
|
|
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
|
|
|
|
|
|
|
|
|
baseVaultAccountIndex := instruction.Accounts[6]
|
|
|
|
|
quoteVaultAccountIndex := instruction.Accounts[7]
|
|
|
|
|
|
|
|
|
|
userBaseVaultAccountIndex := instruction.Accounts[9]
|
|
|
|
|
userQuoteVaultAccountIndex := instruction.Accounts[10]
|
|
|
|
|
|
|
|
|
|
baseTokenbalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultAccountIndex)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("failed to get base vault balance after tx: %v", err)
|
|
|
|
|
}
|
|
|
|
|
quoteTokenbalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultAccountIndex)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("failed to get quote vault balance after tx: %v", err)
|
|
|
|
|
}
|
|
|
|
|
inners, err := getInnerInstructions(innerInstructions, offset[1])
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), err
|
|
|
|
|
}
|
|
|
|
|
var nextIndex int
|
|
|
|
|
var baseFound, quoteFound bool
|
|
|
|
|
var baseAmount, quoteAmount decimal.Decimal
|
|
|
|
|
for i, inner := range inners {
|
|
|
|
|
from, to, amount, err := parseTokenTransfer(tx.rawTx, inner)
|
|
|
|
|
if err != nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if from.Equals(tx.rawTx.accountList[userBaseVaultAccountIndex]) && to.Equals(tx.rawTx.accountList[baseVaultAccountIndex]) && !baseFound {
|
|
|
|
|
baseAmount = decimal.NewFromUint64(amount)
|
|
|
|
|
baseFound = true
|
|
|
|
|
} else if from.Equals(tx.rawTx.accountList[userQuoteVaultAccountIndex]) && to.Equals(tx.rawTx.accountList[quoteVaultAccountIndex]) && !quoteFound {
|
|
|
|
|
quoteAmount = decimal.NewFromUint64(amount)
|
|
|
|
|
quoteFound = true
|
|
|
|
|
}
|
|
|
|
|
if baseFound && quoteFound {
|
|
|
|
|
nextIndex = i + 1
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !baseFound || !quoteFound {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("failed to find token transfer inner instruction for add liquidity, offset %d, %d", offset[0], offset[1])
|
|
|
|
|
}
|
|
|
|
|
offset[1] += uint(nextIndex + 1)
|
|
|
|
|
baseReserve, _ := decimal.NewFromString(baseTokenbalance.UITokenAmount.Amount)
|
|
|
|
|
quoteReserve, _ := decimal.NewFromString(quoteTokenbalance.UITokenAmount.Amount)
|
|
|
|
|
return []Swap{
|
|
|
|
|
{
|
|
|
|
|
Program: SolProgramRaydiumV4,
|
|
|
|
|
Event: "remove_liquidity",
|
|
|
|
|
Pool: tx.rawTx.accountList[instruction.Accounts[1]],
|
|
|
|
|
BaseMint: baseTokenbalance.MintAccount,
|
|
|
|
|
QuoteMint: quoteTokenbalance.MintAccount,
|
|
|
|
|
BaseTokenProgram: baseTokenbalance.ProgramIDAccount,
|
|
|
|
|
QuoteTokenProgram: quoteTokenbalance.ProgramIDAccount,
|
|
|
|
|
User: tx.rawTx.accountList[instruction.Accounts[12]],
|
|
|
|
|
BaseMintDecimals: uint8(baseTokenbalance.UITokenAmount.Decimals),
|
|
|
|
|
QuoteMintDecimals: uint8(quoteTokenbalance.UITokenAmount.Decimals),
|
|
|
|
|
BaseReserve: baseReserve,
|
|
|
|
|
QuoteReserve: quoteReserve,
|
|
|
|
|
BaseAmount: baseAmount,
|
|
|
|
|
QuoteAmount: quoteAmount,
|
|
|
|
|
EntryContract: entryContract,
|
|
|
|
|
},
|
|
|
|
|
}, offset, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func raydiumv4RemoveLiquidityParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
|
|
|
|
accountsLen := len(instruction.Accounts)
|
|
|
|
|
const AccountLen = 20
|
|
|
|
|
if accountsLen != AccountLen && accountsLen != AccountLen+1 && accountsLen != AccountLen+2 && accountsLen != AccountLen+3 {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("invalid number of accounts for raydiumv4 add liquidity instruction, offset %d, %d", offset[0], offset[1])
|
|
|
|
|
}
|
|
|
|
|
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
|
|
|
|
baseVaultAccountIndex := instruction.Accounts[6]
|
|
|
|
|
quoteVaultAccountIndex := instruction.Accounts[7]
|
|
|
|
|
userBaseVaultAccountIndex := instruction.Accounts[14]
|
|
|
|
|
userQuoteVaultAccountIndex := instruction.Accounts[16]
|
|
|
|
|
|
|
|
|
|
if accountsLen == AccountLen+2 || accountsLen == AccountLen+3 {
|
|
|
|
|
userBaseVaultAccountIndex = instruction.Accounts[16]
|
|
|
|
|
userQuoteVaultAccountIndex = instruction.Accounts[17]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
baseTokenbalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultAccountIndex)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("failed to get base vault balance after tx: %v", err)
|
|
|
|
|
}
|
|
|
|
|
quoteTokenbalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultAccountIndex)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("failed to get quote vault balance after tx: %v", err)
|
|
|
|
|
}
|
|
|
|
|
inners, err := getInnerInstructions(innerInstructions, offset[1])
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), err
|
|
|
|
|
}
|
|
|
|
|
var nextIndex int
|
|
|
|
|
var baseFound, quoteFound bool
|
|
|
|
|
var baseAmount, quoteAmount decimal.Decimal
|
|
|
|
|
for i, inner := range inners {
|
|
|
|
|
from, to, amount, err := parseTokenTransfer(tx.rawTx, inner)
|
|
|
|
|
if err != nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if to.Equals(tx.rawTx.accountList[userBaseVaultAccountIndex]) && from.Equals(tx.rawTx.accountList[baseVaultAccountIndex]) && !baseFound {
|
|
|
|
|
baseAmount = decimal.NewFromUint64(amount)
|
|
|
|
|
baseFound = true
|
|
|
|
|
} else if to.Equals(tx.rawTx.accountList[userQuoteVaultAccountIndex]) && from.Equals(tx.rawTx.accountList[quoteVaultAccountIndex]) && !quoteFound {
|
|
|
|
|
quoteAmount = decimal.NewFromUint64(amount)
|
|
|
|
|
quoteFound = true
|
|
|
|
|
}
|
|
|
|
|
if baseFound && quoteFound {
|
|
|
|
|
nextIndex = i + 1
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !baseFound || !quoteFound {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("failed to find token transfer inner instruction for add liquidity, offset %d, %d", offset[0], offset[1])
|
|
|
|
|
}
|
|
|
|
|
offset[1] += uint(nextIndex + 1)
|
|
|
|
|
baseReserve, _ := decimal.NewFromString(baseTokenbalance.UITokenAmount.Amount)
|
|
|
|
|
quoteReserve, _ := decimal.NewFromString(quoteTokenbalance.UITokenAmount.Amount)
|
|
|
|
|
return []Swap{
|
|
|
|
|
{
|
|
|
|
|
Program: SolProgramRaydiumV4,
|
|
|
|
|
Event: "remove_liquidity",
|
|
|
|
|
Pool: tx.rawTx.accountList[instruction.Accounts[1]],
|
|
|
|
|
BaseMint: baseTokenbalance.MintAccount,
|
|
|
|
|
QuoteMint: quoteTokenbalance.MintAccount,
|
|
|
|
|
BaseTokenProgram: baseTokenbalance.ProgramIDAccount,
|
|
|
|
|
QuoteTokenProgram: quoteTokenbalance.ProgramIDAccount,
|
|
|
|
|
User: tx.rawTx.accountList[0],
|
|
|
|
|
BaseMintDecimals: uint8(baseTokenbalance.UITokenAmount.Decimals),
|
|
|
|
|
QuoteMintDecimals: uint8(quoteTokenbalance.UITokenAmount.Decimals),
|
|
|
|
|
BaseReserve: baseReserve,
|
|
|
|
|
QuoteReserve: quoteReserve,
|
|
|
|
|
BaseAmount: baseAmount,
|
|
|
|
|
QuoteAmount: quoteAmount,
|
|
|
|
|
EntryContract: entryContract,
|
|
|
|
|
},
|
|
|
|
|
}, offset, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func raydiumv4WithdrawPNLParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
|
|
|
|
accountsLen := len(instruction.Accounts)
|
|
|
|
|
if accountsLen != 17 && accountsLen != 18 {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("invalid number of accounts for raydiumv4 WithdrawPNL instruction, offset %d, %d", offset[0], offset[1])
|
|
|
|
|
}
|
|
|
|
|
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
|
|
|
|
|
|
|
|
|
baseVaultAccountIndex := instruction.Accounts[5]
|
|
|
|
|
quoteVaultAccountIndex := instruction.Accounts[6]
|
|
|
|
|
userBaseVaultAccountIndex := instruction.Accounts[7]
|
|
|
|
|
userQuoteVaultAccountIndex := instruction.Accounts[8]
|
|
|
|
|
|
|
|
|
|
baseTokenbalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultAccountIndex)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("failed to get base vault balance after tx: %v", err)
|
|
|
|
|
}
|
|
|
|
|
quoteTokenbalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultAccountIndex)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("failed to get quote vault balance after tx: %v", err)
|
|
|
|
|
}
|
|
|
|
|
inners, err := getInnerInstructions(innerInstructions, offset[1])
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), err
|
|
|
|
|
}
|
|
|
|
|
var nextIndex int
|
|
|
|
|
var baseFound, quoteFound bool
|
|
|
|
|
var baseAmount, quoteAmount decimal.Decimal
|
|
|
|
|
for i, inner := range inners {
|
|
|
|
|
from, to, amount, err := parseTokenTransfer(tx.rawTx, inner)
|
|
|
|
|
if err != nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if to.Equals(tx.rawTx.accountList[userBaseVaultAccountIndex]) && from.Equals(tx.rawTx.accountList[baseVaultAccountIndex]) && !baseFound {
|
|
|
|
|
baseAmount = decimal.NewFromUint64(amount)
|
|
|
|
|
baseFound = true
|
|
|
|
|
} else if to.Equals(tx.rawTx.accountList[userQuoteVaultAccountIndex]) && from.Equals(tx.rawTx.accountList[quoteVaultAccountIndex]) && !quoteFound {
|
|
|
|
|
quoteAmount = decimal.NewFromUint64(amount)
|
|
|
|
|
quoteFound = true
|
|
|
|
|
}
|
|
|
|
|
if baseFound && quoteFound {
|
|
|
|
|
nextIndex = i + 1
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !baseFound || !quoteFound {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("failed to find token transfer inner instruction for with pnl, offset %d, %d", offset[0], offset[1])
|
|
|
|
|
}
|
|
|
|
|
offset[1] += uint(nextIndex + 1)
|
|
|
|
|
|
|
|
|
|
baseReserve, _ := decimal.NewFromString(baseTokenbalance.UITokenAmount.Amount)
|
|
|
|
|
quoteReserve, _ := decimal.NewFromString(quoteTokenbalance.UITokenAmount.Amount)
|
|
|
|
|
return []Swap{
|
|
|
|
|
{
|
|
|
|
|
Program: SolProgramRaydiumV4,
|
|
|
|
|
Event: "remove_liquidity",
|
|
|
|
|
Pool: tx.rawTx.accountList[instruction.Accounts[1]],
|
|
|
|
|
BaseMint: baseTokenbalance.MintAccount,
|
|
|
|
|
QuoteMint: quoteTokenbalance.MintAccount,
|
|
|
|
|
BaseTokenProgram: baseTokenbalance.ProgramIDAccount,
|
|
|
|
|
QuoteTokenProgram: quoteTokenbalance.ProgramIDAccount,
|
|
|
|
|
User: tx.rawTx.accountList[instruction.Accounts[9]],
|
|
|
|
|
BaseMintDecimals: uint8(baseTokenbalance.UITokenAmount.Decimals),
|
|
|
|
|
QuoteMintDecimals: uint8(quoteTokenbalance.UITokenAmount.Decimals),
|
|
|
|
|
BaseReserve: baseReserve,
|
|
|
|
|
QuoteReserve: quoteReserve,
|
|
|
|
|
BaseAmount: baseAmount,
|
|
|
|
|
QuoteAmount: quoteAmount,
|
|
|
|
|
EntryContract: entryContract,
|
|
|
|
|
},
|
|
|
|
|
}, offset, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func raydiumv4SwapParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
|
|
|
|
accountsLen := len(instruction.Accounts)
|
|
|
|
|
if accountsLen != 17 && accountsLen != 18 {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("invalid number of accounts for raydiumv4 swap instruction, offset %d, %d", offset[0], offset[1])
|
|
|
|
|
}
|
|
|
|
|
user := tx.rawTx.accountList[instruction.Accounts[accountsLen-1]]
|
|
|
|
|
userSrcIdx := instruction.Accounts[accountsLen-3]
|
|
|
|
|
userDestIdx := instruction.Accounts[accountsLen-2]
|
|
|
|
|
vaultBaseIdx := instruction.Accounts[4]
|
|
|
|
|
vaultQuoteIdx := instruction.Accounts[5]
|
|
|
|
|
if accountsLen == 18 {
|
|
|
|
|
vaultBaseIdx = instruction.Accounts[5]
|
|
|
|
|
vaultQuoteIdx = instruction.Accounts[6]
|
|
|
|
|
}
|
|
|
|
|
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
|
|
|
|
|
|
|
|
|
ammAccount := tx.rawTx.accountList[instruction.Accounts[1]]
|
|
|
|
|
|
|
|
|
|
userSourceTokenAccount := tx.rawTx.accountList[userSrcIdx]
|
|
|
|
|
userDestinationTokenAccount := tx.rawTx.accountList[userDestIdx]
|
|
|
|
|
baseTokenbalance, err := getTokenBalanceAfterTx(tx.rawTx, vaultBaseIdx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("failed to get base vault balance after tx: %v", err)
|
|
|
|
|
}
|
|
|
|
|
quoteTokenbalance, err := getTokenBalanceAfterTx(tx.rawTx, vaultQuoteIdx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("failed to get quote vault balance after tx: %v", err)
|
|
|
|
|
}
|
|
|
|
|
inners, err := getInnerInstructions(innerInstructions, offset[1])
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), err
|
|
|
|
|
}
|
|
|
|
|
var nextIndex int
|
|
|
|
|
var srcFound, destFound bool
|
|
|
|
|
var baseAmount, quoteAmount decimal.Decimal
|
|
|
|
|
var event string
|
|
|
|
|
for i, inner := range inners {
|
|
|
|
|
from, to, amount, err := parseTokenTransfer(tx.rawTx, inner)
|
|
|
|
|
if err != nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if from.Equals(userSourceTokenAccount) && !srcFound {
|
|
|
|
|
if to.Equals(tx.rawTx.accountList[vaultBaseIdx]) {
|
|
|
|
|
event = "sell"
|
|
|
|
|
baseAmount = decimal.NewFromUint64(amount)
|
|
|
|
|
srcFound = true
|
|
|
|
|
} else if to.Equals(tx.rawTx.accountList[vaultQuoteIdx]) {
|
|
|
|
|
event = "buy"
|
|
|
|
|
quoteAmount = decimal.NewFromUint64(amount)
|
|
|
|
|
srcFound = true
|
|
|
|
|
}
|
|
|
|
|
} else if to.Equals(userDestinationTokenAccount) && !destFound {
|
|
|
|
|
if from.Equals(tx.rawTx.accountList[vaultQuoteIdx]) {
|
|
|
|
|
event = "sell"
|
|
|
|
|
quoteAmount = decimal.NewFromUint64(amount)
|
|
|
|
|
destFound = true
|
|
|
|
|
} else if from.Equals(tx.rawTx.accountList[vaultBaseIdx]) {
|
|
|
|
|
event = "buy"
|
|
|
|
|
baseAmount = decimal.NewFromUint64(amount)
|
|
|
|
|
destFound = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
if srcFound && destFound {
|
|
|
|
|
nextIndex = i + 1
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !srcFound || !destFound {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("raydiumv4 failed to find token transfer inner instruction for swap, offset %d, %d", offset[0], offset[1])
|
|
|
|
|
}
|
|
|
|
|
offset[1] += uint(nextIndex + 1)
|
|
|
|
|
userBase := getAccountBalanceAfterTx(tx.rawTx, userSrcIdx)
|
|
|
|
|
userQuote := getAccountBalanceAfterTx(tx.rawTx, userDestIdx)
|
|
|
|
|
baseReserve, _ := decimal.NewFromString(baseTokenbalance.UITokenAmount.Amount)
|
|
|
|
|
quoteReserve, _ := decimal.NewFromString(quoteTokenbalance.UITokenAmount.Amount)
|
|
|
|
|
|
|
|
|
|
return []Swap{
|
|
|
|
|
{
|
|
|
|
|
Program: SolProgramRaydiumV4,
|
|
|
|
|
Event: event,
|
|
|
|
|
Pool: ammAccount,
|
|
|
|
|
BaseMint: baseTokenbalance.MintAccount,
|
|
|
|
|
QuoteMint: quoteTokenbalance.MintAccount,
|
|
|
|
|
BaseTokenProgram: baseTokenbalance.ProgramIDAccount,
|
|
|
|
|
QuoteTokenProgram: quoteTokenbalance.ProgramIDAccount,
|
|
|
|
|
BaseMintDecimals: uint8(baseTokenbalance.UITokenAmount.Decimals),
|
|
|
|
|
QuoteMintDecimals: uint8(quoteTokenbalance.UITokenAmount.Decimals),
|
|
|
|
|
User: user,
|
|
|
|
|
BaseAmount: baseAmount,
|
|
|
|
|
QuoteAmount: quoteAmount,
|
|
|
|
|
BaseReserve: baseReserve,
|
|
|
|
|
QuoteReserve: quoteReserve,
|
|
|
|
|
Mayhem: false,
|
|
|
|
|
UserBaseBalance: userBase,
|
|
|
|
|
UserQuoteBalance: userQuote,
|
|
|
|
|
EntryContract: entryContract,
|
|
|
|
|
},
|
|
|
|
|
}, offset, nil
|
|
|
|
|
}
|
2026-03-12 16:21:38 +08:00
|
|
|
|
|
|
|
|
func raydiumv4SwapV2Parser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
|
|
|
|
accountsLen := len(instruction.Accounts)
|
2026-04-16 11:39:15 +08:00
|
|
|
if accountsLen < 8 {
|
2026-03-12 16:21:38 +08:00
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("invalid number of accounts for raydiumv4 swapv2 instruction, offset %d, %d", offset[0], offset[1])
|
|
|
|
|
}
|
|
|
|
|
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
|
|
|
|
|
2026-04-16 11:39:15 +08:00
|
|
|
// Raydium's documented V2 layout uses the first 8 accounts. Routed CPI calls
|
|
|
|
|
// may append extra readonly accounts (for example the Raydium program id) at
|
|
|
|
|
// the tail, so we only require the canonical prefix here.
|
2026-03-12 16:21:38 +08:00
|
|
|
ammAccount := tx.rawTx.accountList[instruction.Accounts[1]]
|
|
|
|
|
user := tx.rawTx.accountList[instruction.Accounts[7]]
|
|
|
|
|
userSourceTokenAccount := tx.rawTx.accountList[instruction.Accounts[5]]
|
|
|
|
|
userDestinationTokenAccount := tx.rawTx.accountList[instruction.Accounts[6]]
|
|
|
|
|
baseVaultIdx := instruction.Accounts[3]
|
|
|
|
|
quoteVaultIdx := instruction.Accounts[4]
|
|
|
|
|
|
|
|
|
|
baseTokenbalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultIdx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("failed to get base vault balance after tx: %v", err)
|
|
|
|
|
}
|
|
|
|
|
quoteTokenbalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultIdx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("failed to get quote vault balance after tx: %v", err)
|
|
|
|
|
}
|
|
|
|
|
inners, err := getInnerInstructions(innerInstructions, offset[1])
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var nextIndex int
|
|
|
|
|
var srcFound, destFound bool
|
|
|
|
|
var baseAmount, quoteAmount decimal.Decimal
|
|
|
|
|
var event string
|
|
|
|
|
for i, inner := range inners {
|
|
|
|
|
from, to, amount, err := parseTokenTransfer(tx.rawTx, inner)
|
|
|
|
|
if err != nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if from.Equals(userSourceTokenAccount) && !srcFound {
|
|
|
|
|
if to.Equals(tx.rawTx.accountList[baseVaultIdx]) {
|
|
|
|
|
event = "sell"
|
|
|
|
|
baseAmount = decimal.NewFromUint64(amount)
|
|
|
|
|
srcFound = true
|
|
|
|
|
} else if to.Equals(tx.rawTx.accountList[quoteVaultIdx]) {
|
|
|
|
|
event = "buy"
|
|
|
|
|
quoteAmount = decimal.NewFromUint64(amount)
|
|
|
|
|
srcFound = true
|
|
|
|
|
}
|
|
|
|
|
} else if to.Equals(userDestinationTokenAccount) && !destFound {
|
|
|
|
|
if from.Equals(tx.rawTx.accountList[quoteVaultIdx]) {
|
|
|
|
|
event = "sell"
|
|
|
|
|
quoteAmount = decimal.NewFromUint64(amount)
|
|
|
|
|
destFound = true
|
|
|
|
|
} else if from.Equals(tx.rawTx.accountList[baseVaultIdx]) {
|
|
|
|
|
event = "buy"
|
|
|
|
|
baseAmount = decimal.NewFromUint64(amount)
|
|
|
|
|
destFound = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if srcFound && destFound {
|
|
|
|
|
nextIndex = i + 1
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !srcFound || !destFound {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("raydiumv4 failed to find token transfer inner instruction for swapv2, offset %d, %d", offset[0], offset[1])
|
|
|
|
|
}
|
|
|
|
|
offset[1] += uint(nextIndex + 1)
|
|
|
|
|
|
|
|
|
|
userBase := getAccountBalanceAfterTx(tx.rawTx, instruction.Accounts[5])
|
|
|
|
|
userQuote := getAccountBalanceAfterTx(tx.rawTx, instruction.Accounts[6])
|
|
|
|
|
baseReserve, _ := decimal.NewFromString(baseTokenbalance.UITokenAmount.Amount)
|
|
|
|
|
quoteReserve, _ := decimal.NewFromString(quoteTokenbalance.UITokenAmount.Amount)
|
|
|
|
|
|
|
|
|
|
return []Swap{
|
|
|
|
|
{
|
|
|
|
|
Program: SolProgramRaydiumV4,
|
|
|
|
|
Event: event,
|
|
|
|
|
Pool: ammAccount,
|
|
|
|
|
BaseMint: baseTokenbalance.MintAccount,
|
|
|
|
|
QuoteMint: quoteTokenbalance.MintAccount,
|
|
|
|
|
BaseTokenProgram: baseTokenbalance.ProgramIDAccount,
|
|
|
|
|
QuoteTokenProgram: quoteTokenbalance.ProgramIDAccount,
|
|
|
|
|
BaseMintDecimals: uint8(baseTokenbalance.UITokenAmount.Decimals),
|
|
|
|
|
QuoteMintDecimals: uint8(quoteTokenbalance.UITokenAmount.Decimals),
|
|
|
|
|
User: user,
|
|
|
|
|
BaseAmount: baseAmount,
|
|
|
|
|
QuoteAmount: quoteAmount,
|
|
|
|
|
BaseReserve: baseReserve,
|
|
|
|
|
QuoteReserve: quoteReserve,
|
|
|
|
|
Mayhem: false,
|
|
|
|
|
UserBaseBalance: userBase,
|
|
|
|
|
UserQuoteBalance: userQuote,
|
|
|
|
|
EntryContract: entryContract,
|
|
|
|
|
},
|
|
|
|
|
}, offset, nil
|
|
|
|
|
}
|