2026-02-09 14:46:19 +08:00
|
|
|
package pump_parser
|
|
|
|
|
|
|
|
|
|
import (
|
2026-04-16 14:24:14 +08:00
|
|
|
"encoding/binary"
|
2026-02-09 14:46:19 +08:00
|
|
|
"fmt"
|
|
|
|
|
|
|
|
|
|
"github.com/gagliardetto/solana-go"
|
|
|
|
|
"github.com/shopspring/decimal"
|
|
|
|
|
)
|
|
|
|
|
|
2026-04-16 14:24:14 +08:00
|
|
|
func decodeOrcaWhirlpoolSwapArgs(data []byte) (amount uint64, otherAmountThreshold uint64, amountSpecifiedIsInput bool, err error) {
|
|
|
|
|
if len(data) < 42 {
|
|
|
|
|
return 0, 0, false, fmt.Errorf("orca whirlpool swap instruction data too short")
|
|
|
|
|
}
|
|
|
|
|
amount = binary.LittleEndian.Uint64(data[8:16])
|
|
|
|
|
otherAmountThreshold = binary.LittleEndian.Uint64(data[16:24])
|
|
|
|
|
amountSpecifiedIsInput = data[40] != 0
|
|
|
|
|
return amount, otherAmountThreshold, amountSpecifiedIsInput, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func decodeOrcaWhirlpoolTwoHopSwapArgs(data []byte) (amount uint64, otherAmountThreshold uint64, amountSpecifiedIsInput bool, err error) {
|
|
|
|
|
if len(data) < 27 {
|
|
|
|
|
return 0, 0, false, fmt.Errorf("orca whirlpool two-hop swap instruction data too short")
|
|
|
|
|
}
|
|
|
|
|
amount = binary.LittleEndian.Uint64(data[8:16])
|
|
|
|
|
otherAmountThreshold = binary.LittleEndian.Uint64(data[16:24])
|
|
|
|
|
amountSpecifiedIsInput = data[24] != 0
|
|
|
|
|
return amount, otherAmountThreshold, amountSpecifiedIsInput, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 14:46:19 +08:00
|
|
|
func orcaWhirPoolParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
|
|
|
|
if !tx.rawTx.accountList[instruction.ProgramIDIndex].Equals(orcaProgramID) {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("orcawhirpoolprogram instruction not found, offset, %d, %d", offset[0], offset[1])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
decode := instruction.Data
|
|
|
|
|
|
|
|
|
|
discriminator := *(*[8]byte)(decode[:8])
|
|
|
|
|
|
|
|
|
|
switch discriminator {
|
|
|
|
|
case orcaInitializePoolDiscriminator:
|
|
|
|
|
return orcaWhirPoolInitialParser(tx, instruction, innerInstructions, offset)
|
|
|
|
|
case orcaInitializePoolWithAdaptiveFeeDiscriminator:
|
|
|
|
|
return orcaWhirPoolInitialWithAdaptiveFeeParser(tx, instruction, innerInstructions, offset)
|
|
|
|
|
case orcaInitializePoolV2Discriminator:
|
|
|
|
|
return orcaWhirPoolInitialV2Parser(tx, instruction, innerInstructions, offset)
|
|
|
|
|
case orcaIncreaseLiquidityDiscriminator, orcaDecreaseLiquidityDiscriminator:
|
|
|
|
|
return orcaWhirPoolLiquidityParser(tx, instruction, innerInstructions, offset)
|
|
|
|
|
case orcaIncreaseLiquidityV2Discriminator, orcaDecreaseLiquidityV2Discriminator:
|
|
|
|
|
return orcaWhirPoolLiquidityV2Parser(tx, instruction, innerInstructions, offset)
|
|
|
|
|
case orcaCollectFeesDiscriminator:
|
|
|
|
|
return orcaWhirPoolCollectFeeParser(tx, instruction, innerInstructions, offset)
|
|
|
|
|
case orcaCollectFeesV2Discriminator:
|
|
|
|
|
return orcaWhirPoolCollectFeeV2Parser(tx, instruction, innerInstructions, offset)
|
|
|
|
|
case orcaCollectProtocolFeesV2Discriminator:
|
|
|
|
|
return orcaWhirPoolCollectProtocolFeeV2Parser(tx, instruction, innerInstructions, offset)
|
|
|
|
|
case orcaSwapDiscriminator:
|
|
|
|
|
return orcaWhirPoolSwapParser(tx, instruction, innerInstructions, offset)
|
|
|
|
|
case orcaSwapV2Discriminator:
|
|
|
|
|
return orcaWhirPoolSwapV2Parser(tx, instruction, innerInstructions, offset)
|
|
|
|
|
case orcaTwoHopSwapDiscriminator:
|
|
|
|
|
return orcaWhirPoolTwoHopSwapParser(tx, instruction, innerInstructions, offset)
|
|
|
|
|
case orcaTwoHopSwapV2Discriminator:
|
|
|
|
|
return orcaWhirPoolTwoHopSwapV2Parser(tx, instruction, innerInstructions, offset)
|
|
|
|
|
default:
|
|
|
|
|
return nil, increaseOffset(offset), InstructionIgnoredError
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func orcaWhirPoolInitialParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
|
|
|
|
if len(instruction.Accounts) < 11 {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for initialize pool instruction")
|
|
|
|
|
}
|
|
|
|
|
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
|
|
|
|
|
|
|
|
|
// Get accounts from instruction
|
|
|
|
|
pool := tx.rawTx.accountList[instruction.Accounts[4]]
|
|
|
|
|
signer := tx.rawTx.accountList[0]
|
|
|
|
|
|
|
|
|
|
vault0 := instruction.Accounts[5]
|
|
|
|
|
vault1 := instruction.Accounts[6]
|
|
|
|
|
|
|
|
|
|
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault0)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("failed to get token0 vault balance after tx: %v", err)
|
|
|
|
|
}
|
|
|
|
|
quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault1)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("failed to get token1 vault balance after tx: %v", err)
|
|
|
|
|
}
|
|
|
|
|
baseReserve, _ := decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount)
|
|
|
|
|
quoteReserve, _ := decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount)
|
|
|
|
|
|
|
|
|
|
offset[1] += 5
|
|
|
|
|
return []Swap{
|
|
|
|
|
{
|
|
|
|
|
Program: SolProgramOrcaWhirPool,
|
|
|
|
|
Event: "create",
|
|
|
|
|
Pool: pool,
|
|
|
|
|
BaseMint: baseTokenBalance.MintAccount,
|
|
|
|
|
QuoteMint: quoteTokenBalance.MintAccount,
|
|
|
|
|
BaseTokenProgram: baseTokenBalance.ProgramIDAccount,
|
|
|
|
|
QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount,
|
|
|
|
|
Creator: signer,
|
|
|
|
|
BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals),
|
|
|
|
|
QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals),
|
|
|
|
|
BaseReserve: baseReserve,
|
|
|
|
|
QuoteReserve: quoteReserve,
|
|
|
|
|
User: tx.rawTx.accountList[instruction.Accounts[0]],
|
|
|
|
|
EntryContract: entryContract,
|
|
|
|
|
},
|
|
|
|
|
}, offset, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func orcaWhirPoolInitialWithAdaptiveFeeParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
|
|
|
|
if len(instruction.Accounts) < 16 {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for initialize pool instruction")
|
|
|
|
|
}
|
|
|
|
|
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
|
|
|
|
|
|
|
|
|
pool := tx.rawTx.accountList[instruction.Accounts[7]]
|
|
|
|
|
signer := tx.rawTx.accountList[0]
|
|
|
|
|
|
|
|
|
|
vault0 := instruction.Accounts[9]
|
|
|
|
|
vault1 := instruction.Accounts[10]
|
|
|
|
|
|
|
|
|
|
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault0)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("failed to get token0 vault balance after tx: %v", err)
|
|
|
|
|
}
|
|
|
|
|
quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault1)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("failed to get token1 vault balance after tx: %v", err)
|
|
|
|
|
}
|
|
|
|
|
baseReserve, _ := decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount)
|
|
|
|
|
quoteReserve, _ := decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount)
|
|
|
|
|
offset[1] += 9
|
|
|
|
|
return []Swap{
|
|
|
|
|
{
|
|
|
|
|
Program: SolProgramOrcaWhirPool,
|
|
|
|
|
Event: "create",
|
|
|
|
|
Pool: pool,
|
|
|
|
|
BaseMint: baseTokenBalance.MintAccount,
|
|
|
|
|
QuoteMint: quoteTokenBalance.MintAccount,
|
|
|
|
|
BaseTokenProgram: baseTokenBalance.ProgramIDAccount,
|
|
|
|
|
QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount,
|
|
|
|
|
Creator: signer,
|
|
|
|
|
BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals),
|
|
|
|
|
QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals),
|
|
|
|
|
BaseReserve: baseReserve,
|
|
|
|
|
QuoteReserve: quoteReserve,
|
|
|
|
|
User: tx.rawTx.accountList[instruction.Accounts[0]],
|
|
|
|
|
EntryContract: entryContract,
|
|
|
|
|
},
|
|
|
|
|
}, offset, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func orcaWhirPoolInitialV2Parser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
|
|
|
|
if len(instruction.Accounts) < 14 {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for initialize pool instruction")
|
|
|
|
|
}
|
|
|
|
|
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
|
|
|
|
|
|
|
|
|
pool := tx.rawTx.accountList[instruction.Accounts[6]]
|
|
|
|
|
signer := tx.rawTx.accountList[0]
|
|
|
|
|
|
|
|
|
|
vault0 := instruction.Accounts[7]
|
|
|
|
|
vault1 := instruction.Accounts[8]
|
|
|
|
|
|
|
|
|
|
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault0)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("failed to get token0 vault balance after tx: %v", err)
|
|
|
|
|
}
|
|
|
|
|
quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault1)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("failed to get token1 vault balance after tx: %v", err)
|
|
|
|
|
}
|
|
|
|
|
baseReserve, _ := decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount)
|
|
|
|
|
quoteReserve, _ := decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount)
|
|
|
|
|
offset[1] += 7
|
|
|
|
|
return []Swap{
|
|
|
|
|
{
|
|
|
|
|
Program: SolProgramOrcaWhirPool,
|
|
|
|
|
Event: "create",
|
|
|
|
|
Pool: pool,
|
|
|
|
|
BaseMint: baseTokenBalance.MintAccount,
|
|
|
|
|
QuoteMint: quoteTokenBalance.MintAccount,
|
|
|
|
|
BaseTokenProgram: baseTokenBalance.ProgramIDAccount,
|
|
|
|
|
QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount,
|
|
|
|
|
Creator: signer,
|
|
|
|
|
BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals),
|
|
|
|
|
QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals),
|
|
|
|
|
BaseReserve: baseReserve,
|
|
|
|
|
QuoteReserve: quoteReserve,
|
|
|
|
|
User: tx.rawTx.accountList[instruction.Accounts[0]],
|
|
|
|
|
EntryContract: entryContract,
|
|
|
|
|
},
|
|
|
|
|
}, offset, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func orcaWhirPoolLiquidityParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
|
|
|
|
if len(instruction.Accounts) < 11 {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for instruction")
|
|
|
|
|
}
|
|
|
|
|
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
|
|
|
|
signer := tx.rawTx.accountList[0]
|
|
|
|
|
pool := tx.rawTx.accountList[instruction.Accounts[0]]
|
|
|
|
|
|
|
|
|
|
vault0 := instruction.Accounts[7]
|
|
|
|
|
vault1 := instruction.Accounts[8]
|
|
|
|
|
|
|
|
|
|
baseMint := tx.rawTx.accountList[instruction.Accounts[1]]
|
|
|
|
|
quoteMint := tx.rawTx.accountList[instruction.Accounts[2]]
|
|
|
|
|
|
|
|
|
|
baseTokenBalance, _ := getTokenBalanceAfterTx(tx.rawTx, vault0)
|
|
|
|
|
quoteTokenBalance, _ := getTokenBalanceAfterTx(tx.rawTx, vault1)
|
|
|
|
|
if baseTokenBalance == nil && quoteTokenBalance == nil {
|
|
|
|
|
return nil, increaseOffset(offset), InstructionIgnoredError
|
|
|
|
|
}
|
|
|
|
|
var (
|
|
|
|
|
baseReserve decimal.Decimal
|
|
|
|
|
quoteReserve decimal.Decimal
|
|
|
|
|
|
|
|
|
|
baseMintDecimals uint8
|
|
|
|
|
quoteMintDecimals uint8
|
|
|
|
|
|
|
|
|
|
baseProgram solana.PublicKey
|
|
|
|
|
quoteProgram solana.PublicKey
|
|
|
|
|
)
|
|
|
|
|
if baseTokenBalance != nil {
|
|
|
|
|
baseReserve, _ = decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount)
|
|
|
|
|
baseMintDecimals = uint8(baseTokenBalance.UITokenAmount.Decimals)
|
|
|
|
|
baseProgram = baseTokenBalance.ProgramIDAccount
|
|
|
|
|
}
|
|
|
|
|
if quoteTokenBalance != nil {
|
|
|
|
|
quoteReserve, _ = decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount)
|
|
|
|
|
quoteMintDecimals = uint8(quoteTokenBalance.UITokenAmount.Decimals)
|
|
|
|
|
quoteProgram = quoteTokenBalance.ProgramIDAccount
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
discriminator := *(*[8]byte)(instruction.Data[:8])
|
|
|
|
|
var instructionName string
|
|
|
|
|
if discriminator == orcaDecreaseLiquidityDiscriminator {
|
|
|
|
|
instructionName = "remove_liquidity"
|
|
|
|
|
} else if discriminator == orcaIncreaseLiquidityDiscriminator {
|
|
|
|
|
instructionName = "add_liquidity"
|
|
|
|
|
}
|
|
|
|
|
var prefixLen = offset[1]
|
|
|
|
|
inners, err := getInnerInstructions(innerInstructions, prefixLen)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("orca whirpool get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1])
|
|
|
|
|
}
|
|
|
|
|
if len(inners) < 2 {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("not enough inner instructions for protocol fee collection")
|
|
|
|
|
}
|
|
|
|
|
baseAmount := decimal.Zero
|
|
|
|
|
quoteAmount := decimal.Zero
|
|
|
|
|
var baseFound, quoteFound bool
|
|
|
|
|
for i := 0; i < 2; i++ {
|
|
|
|
|
from, to, amount, err := parseTokenTransfer(tx.rawTx, inners[i])
|
|
|
|
|
if err != nil { // maybe momo?
|
|
|
|
|
continue
|
|
|
|
|
//return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initial parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
|
|
|
|
|
}
|
|
|
|
|
if !baseFound && (from.Equals(tx.rawTx.accountList[vault0]) || to.Equals(tx.rawTx.accountList[vault0])) {
|
|
|
|
|
baseAmount = decimal.NewFromInt(int64(amount))
|
|
|
|
|
baseFound = true
|
|
|
|
|
} else if !quoteFound && (from.Equals(tx.rawTx.accountList[vault1]) || to.Equals(tx.rawTx.accountList[vault1])) {
|
|
|
|
|
quoteAmount = decimal.NewFromInt(int64(amount))
|
|
|
|
|
quoteFound = true
|
|
|
|
|
}
|
|
|
|
|
if baseFound && quoteFound {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !baseFound && !quoteFound {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("liquidity change failed to find token transfer for both vaults in inner instructions")
|
|
|
|
|
}
|
|
|
|
|
offset[1] += 2
|
|
|
|
|
if baseAmount.Equal(decimal.Zero) && quoteAmount.Equal(decimal.Zero) {
|
|
|
|
|
return nil, increaseOffset(offset), InstructionIgnoredError
|
|
|
|
|
}
|
|
|
|
|
if baseAmount.Equal(decimal.Zero) || quoteAmount.Equal(decimal.Zero) {
|
|
|
|
|
instructionName += "_on_side"
|
|
|
|
|
}
|
|
|
|
|
if (baseTokenBalance == nil && !baseAmount.Equal(decimal.Zero)) || (quoteTokenBalance == nil && !quoteAmount.Equal(decimal.Zero)) {
|
|
|
|
|
return nil, offset, fmt.Errorf("token balance is nil but amount is not zero")
|
|
|
|
|
}
|
|
|
|
|
return []Swap{
|
|
|
|
|
{
|
|
|
|
|
Program: SolProgramOrcaWhirPool,
|
|
|
|
|
Event: instructionName,
|
|
|
|
|
Pool: pool,
|
|
|
|
|
BaseMint: baseMint,
|
|
|
|
|
QuoteMint: quoteMint,
|
|
|
|
|
BaseTokenProgram: baseProgram,
|
|
|
|
|
QuoteTokenProgram: quoteProgram,
|
|
|
|
|
Creator: signer,
|
|
|
|
|
BaseMintDecimals: baseMintDecimals,
|
|
|
|
|
QuoteMintDecimals: quoteMintDecimals,
|
|
|
|
|
BaseAmount: baseAmount,
|
|
|
|
|
QuoteAmount: quoteAmount,
|
|
|
|
|
BaseReserve: baseReserve,
|
|
|
|
|
QuoteReserve: quoteReserve,
|
|
|
|
|
User: tx.rawTx.accountList[instruction.Accounts[0]],
|
|
|
|
|
EntryContract: entryContract,
|
|
|
|
|
},
|
|
|
|
|
}, offset, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func orcaWhirPoolLiquidityV2Parser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
|
|
|
|
if len(instruction.Accounts) < 15 {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for instruction")
|
|
|
|
|
}
|
|
|
|
|
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
|
|
|
|
signer := tx.rawTx.accountList[0]
|
|
|
|
|
pool := tx.rawTx.accountList[instruction.Accounts[0]]
|
|
|
|
|
baseMint := tx.rawTx.accountList[instruction.Accounts[1]]
|
|
|
|
|
quoteMint := tx.rawTx.accountList[instruction.Accounts[2]]
|
|
|
|
|
vault0 := instruction.Accounts[11]
|
|
|
|
|
vault1 := instruction.Accounts[12]
|
|
|
|
|
|
|
|
|
|
baseTokenBalance, _ := getTokenBalanceAfterTx(tx.rawTx, vault0)
|
|
|
|
|
quoteTokenBalance, _ := getTokenBalanceAfterTx(tx.rawTx, vault1)
|
|
|
|
|
if baseTokenBalance == nil && quoteTokenBalance == nil {
|
|
|
|
|
return nil, increaseOffset(offset), InstructionIgnoredError
|
|
|
|
|
}
|
|
|
|
|
var (
|
|
|
|
|
baseReserve decimal.Decimal
|
|
|
|
|
quoteReserve decimal.Decimal
|
|
|
|
|
|
|
|
|
|
baseMintDecimals uint8
|
|
|
|
|
quoteMintDecimals uint8
|
|
|
|
|
|
|
|
|
|
baseProgram solana.PublicKey
|
|
|
|
|
quoteProgram solana.PublicKey
|
|
|
|
|
)
|
|
|
|
|
if baseTokenBalance != nil {
|
|
|
|
|
baseReserve, _ = decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount)
|
|
|
|
|
baseMintDecimals = uint8(baseTokenBalance.UITokenAmount.Decimals)
|
|
|
|
|
baseProgram = baseTokenBalance.ProgramIDAccount
|
|
|
|
|
}
|
|
|
|
|
if quoteTokenBalance != nil {
|
|
|
|
|
quoteReserve, _ = decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount)
|
|
|
|
|
quoteMintDecimals = uint8(quoteTokenBalance.UITokenAmount.Decimals)
|
|
|
|
|
quoteProgram = quoteTokenBalance.ProgramIDAccount
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
discriminator := *(*[8]byte)(instruction.Data[:8])
|
|
|
|
|
var instructionName string
|
|
|
|
|
if discriminator == orcaDecreaseLiquidityV2Discriminator {
|
|
|
|
|
instructionName = "remove_liquidity"
|
|
|
|
|
} else if discriminator == orcaIncreaseLiquidityV2Discriminator {
|
|
|
|
|
instructionName = "add_liquidity"
|
|
|
|
|
}
|
|
|
|
|
var prefixLen = offset[1]
|
|
|
|
|
inners, err := getInnerInstructions(innerInstructions, prefixLen)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("orca whirpool get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1])
|
|
|
|
|
}
|
|
|
|
|
if len(inners) < 2 {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("not enough inner instructions for protocol fee collection")
|
|
|
|
|
}
|
|
|
|
|
baseAmount := decimal.Zero
|
|
|
|
|
quoteAmount := decimal.Zero
|
|
|
|
|
var baseFound, quoteFound bool
|
|
|
|
|
for i := 0; i < 3; i++ {
|
|
|
|
|
from, to, amount, err := parseTokenTransfer(tx.rawTx, inners[i])
|
|
|
|
|
if err != nil { // maybe momo?
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if !baseFound && (from.Equals(tx.rawTx.accountList[vault0]) || to.Equals(tx.rawTx.accountList[vault0])) {
|
|
|
|
|
baseAmount = decimal.NewFromInt(int64(amount))
|
|
|
|
|
baseFound = true
|
|
|
|
|
} else if !quoteFound && (from.Equals(tx.rawTx.accountList[vault1]) || to.Equals(tx.rawTx.accountList[vault1])) {
|
|
|
|
|
quoteAmount = decimal.NewFromInt(int64(amount))
|
|
|
|
|
quoteFound = true
|
|
|
|
|
}
|
|
|
|
|
if baseFound && quoteFound {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !baseFound && !quoteFound {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("liquidity change failed to find token transfer for both vaults in inner instructions")
|
|
|
|
|
}
|
|
|
|
|
offset[1] += 2
|
|
|
|
|
if baseAmount.Equal(decimal.Zero) && quoteAmount.Equal(decimal.Zero) {
|
|
|
|
|
return nil, offset, InstructionIgnoredError
|
|
|
|
|
}
|
|
|
|
|
if baseAmount.Equal(decimal.Zero) || quoteAmount.Equal(decimal.Zero) {
|
|
|
|
|
instructionName += "_on_side"
|
|
|
|
|
}
|
|
|
|
|
if (baseTokenBalance == nil && !baseAmount.Equal(decimal.Zero)) || (quoteTokenBalance == nil && !quoteAmount.Equal(decimal.Zero)) {
|
|
|
|
|
return nil, offset, fmt.Errorf("token balance is nil but amount is not zero")
|
|
|
|
|
}
|
|
|
|
|
return []Swap{
|
|
|
|
|
{
|
|
|
|
|
Program: SolProgramOrcaWhirPool,
|
|
|
|
|
Event: instructionName,
|
|
|
|
|
Pool: pool,
|
|
|
|
|
BaseMint: baseMint,
|
|
|
|
|
QuoteMint: quoteMint,
|
|
|
|
|
BaseTokenProgram: baseProgram,
|
|
|
|
|
QuoteTokenProgram: quoteProgram,
|
|
|
|
|
Creator: signer,
|
|
|
|
|
BaseMintDecimals: baseMintDecimals,
|
|
|
|
|
QuoteMintDecimals: quoteMintDecimals,
|
|
|
|
|
BaseAmount: baseAmount,
|
|
|
|
|
QuoteAmount: quoteAmount,
|
|
|
|
|
BaseReserve: baseReserve,
|
|
|
|
|
QuoteReserve: quoteReserve,
|
|
|
|
|
User: tx.rawTx.accountList[instruction.Accounts[0]],
|
|
|
|
|
EntryContract: entryContract,
|
|
|
|
|
},
|
|
|
|
|
}, offset, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func orcaWhirPoolCollectFeeParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
|
|
|
|
if len(instruction.Accounts) < 9 {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for initialize pool instruction")
|
|
|
|
|
}
|
|
|
|
|
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
|
|
|
|
signer := tx.rawTx.accountList[0]
|
|
|
|
|
pool := tx.rawTx.accountList[instruction.Accounts[0]]
|
|
|
|
|
vault0 := instruction.Accounts[5]
|
|
|
|
|
vault1 := instruction.Accounts[7]
|
|
|
|
|
|
|
|
|
|
baseTokenBalance, _ := getTokenBalanceAfterTx(tx.rawTx, vault0)
|
|
|
|
|
quoteTokenBalance, _ := getTokenBalanceAfterTx(tx.rawTx, vault1)
|
|
|
|
|
if baseTokenBalance == nil && quoteTokenBalance == nil {
|
|
|
|
|
return nil, increaseOffset(offset), InstructionIgnoredError
|
|
|
|
|
}
|
|
|
|
|
var (
|
|
|
|
|
baseReserve decimal.Decimal
|
|
|
|
|
quoteReserve decimal.Decimal
|
|
|
|
|
|
|
|
|
|
baseMintDecimals uint8
|
|
|
|
|
quoteMintDecimals uint8
|
|
|
|
|
|
|
|
|
|
baseMint solana.PublicKey
|
|
|
|
|
quoteMint solana.PublicKey
|
|
|
|
|
|
|
|
|
|
baseProgram solana.PublicKey
|
|
|
|
|
quoteProgram solana.PublicKey
|
|
|
|
|
)
|
|
|
|
|
if baseTokenBalance != nil {
|
|
|
|
|
baseReserve, _ = decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount)
|
|
|
|
|
baseMintDecimals = uint8(baseTokenBalance.UITokenAmount.Decimals)
|
|
|
|
|
baseProgram = baseTokenBalance.ProgramIDAccount
|
|
|
|
|
baseMint = baseTokenBalance.MintAccount
|
|
|
|
|
}
|
|
|
|
|
if quoteTokenBalance != nil {
|
|
|
|
|
quoteReserve, _ = decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount)
|
|
|
|
|
quoteMintDecimals = uint8(quoteTokenBalance.UITokenAmount.Decimals)
|
|
|
|
|
quoteProgram = quoteTokenBalance.ProgramIDAccount
|
|
|
|
|
quoteMint = quoteTokenBalance.MintAccount
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var instructionName = "remove_liquidity"
|
|
|
|
|
var prefixLen = offset[1]
|
|
|
|
|
inners, err := getInnerInstructions(innerInstructions, prefixLen)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("orca whirpool get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1])
|
|
|
|
|
}
|
|
|
|
|
if len(inners) < 2 {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("not enough inner instructions for protocol fee collection")
|
|
|
|
|
}
|
|
|
|
|
baseAmount := decimal.Zero
|
|
|
|
|
quoteAmount := decimal.Zero
|
|
|
|
|
var baseFound, quoteFound bool
|
|
|
|
|
for i, inner := range inners {
|
|
|
|
|
from, to, amount, err := parseTokenTransfer(tx.rawTx, inner)
|
|
|
|
|
if err != nil {
|
|
|
|
|
continue
|
|
|
|
|
//return nil, increaseOffset(offset), fmt.Errorf("orca whirpool parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
|
|
|
|
|
}
|
|
|
|
|
if !baseFound && (from.Equals(tx.rawTx.accountList[vault0]) || to.Equals(tx.rawTx.accountList[vault0])) {
|
|
|
|
|
baseAmount = decimal.NewFromInt(int64(amount))
|
|
|
|
|
baseFound = true
|
|
|
|
|
} else if !quoteFound && (from.Equals(tx.rawTx.accountList[vault1]) || to.Equals(tx.rawTx.accountList[vault1])) {
|
|
|
|
|
quoteAmount = decimal.NewFromInt(int64(amount))
|
|
|
|
|
quoteFound = true
|
|
|
|
|
}
|
|
|
|
|
if (baseFound && quoteFound) || i >= 6 {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !baseFound && !quoteFound {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("collect fee failed to find token transfer for both vaults in inner instructions")
|
|
|
|
|
}
|
|
|
|
|
offset[1] += 2
|
|
|
|
|
if baseAmount.Equal(decimal.Zero) && quoteAmount.Equal(decimal.Zero) {
|
|
|
|
|
return nil, offset, InstructionIgnoredError
|
|
|
|
|
}
|
|
|
|
|
if baseAmount.Equal(decimal.Zero) || quoteAmount.Equal(decimal.Zero) {
|
|
|
|
|
instructionName += "_on_side"
|
|
|
|
|
}
|
|
|
|
|
if (baseTokenBalance == nil && !baseAmount.Equal(decimal.Zero)) || (quoteTokenBalance == nil && !quoteAmount.Equal(decimal.Zero)) {
|
|
|
|
|
return nil, offset, fmt.Errorf("token balance is nil but amount is not zero")
|
|
|
|
|
}
|
|
|
|
|
return []Swap{
|
|
|
|
|
{
|
|
|
|
|
Program: SolProgramOrcaWhirPool,
|
|
|
|
|
Event: instructionName,
|
|
|
|
|
Pool: pool,
|
|
|
|
|
BaseMint: baseMint,
|
|
|
|
|
QuoteMint: quoteMint,
|
|
|
|
|
BaseTokenProgram: baseProgram,
|
|
|
|
|
QuoteTokenProgram: quoteProgram,
|
|
|
|
|
Creator: signer,
|
|
|
|
|
BaseMintDecimals: baseMintDecimals,
|
|
|
|
|
QuoteMintDecimals: quoteMintDecimals,
|
|
|
|
|
BaseAmount: baseAmount,
|
|
|
|
|
QuoteAmount: quoteAmount,
|
|
|
|
|
BaseReserve: baseReserve,
|
|
|
|
|
QuoteReserve: quoteReserve,
|
|
|
|
|
User: tx.rawTx.accountList[instruction.Accounts[0]],
|
|
|
|
|
EntryContract: entryContract,
|
|
|
|
|
},
|
|
|
|
|
}, offset, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func orcaWhirPoolCollectFeeV2Parser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
|
|
|
|
if len(instruction.Accounts) < 12 {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for initialize pool instruction")
|
|
|
|
|
}
|
|
|
|
|
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
|
|
|
|
signer := tx.rawTx.accountList[0]
|
|
|
|
|
pool := tx.rawTx.accountList[instruction.Accounts[0]]
|
|
|
|
|
baseMint := tx.rawTx.accountList[instruction.Accounts[4]]
|
|
|
|
|
quoteMint := tx.rawTx.accountList[instruction.Accounts[5]]
|
|
|
|
|
vault0 := instruction.Accounts[7]
|
|
|
|
|
vault1 := instruction.Accounts[9]
|
|
|
|
|
|
|
|
|
|
baseTokenBalance, _ := getTokenBalanceAfterTx(tx.rawTx, vault0)
|
|
|
|
|
quoteTokenBalance, _ := getTokenBalanceAfterTx(tx.rawTx, vault1)
|
|
|
|
|
if baseTokenBalance == nil && quoteTokenBalance == nil {
|
|
|
|
|
return nil, increaseOffset(offset), InstructionIgnoredError
|
|
|
|
|
}
|
|
|
|
|
var (
|
|
|
|
|
baseReserve decimal.Decimal
|
|
|
|
|
quoteReserve decimal.Decimal
|
|
|
|
|
|
|
|
|
|
baseMintDecimals uint8
|
|
|
|
|
quoteMintDecimals uint8
|
|
|
|
|
|
|
|
|
|
baseProgram solana.PublicKey
|
|
|
|
|
quoteProgram solana.PublicKey
|
|
|
|
|
)
|
|
|
|
|
if baseTokenBalance != nil {
|
|
|
|
|
baseReserve, _ = decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount)
|
|
|
|
|
baseMintDecimals = uint8(baseTokenBalance.UITokenAmount.Decimals)
|
|
|
|
|
baseProgram = baseTokenBalance.ProgramIDAccount
|
|
|
|
|
}
|
|
|
|
|
if quoteTokenBalance != nil {
|
|
|
|
|
quoteReserve, _ = decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount)
|
|
|
|
|
quoteMintDecimals = uint8(quoteTokenBalance.UITokenAmount.Decimals)
|
|
|
|
|
quoteProgram = quoteTokenBalance.ProgramIDAccount
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var instructionName = "remove_liquidity"
|
|
|
|
|
var prefixLen = offset[1]
|
|
|
|
|
inners, err := getInnerInstructions(innerInstructions, prefixLen)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("orca whirpool get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1])
|
|
|
|
|
}
|
|
|
|
|
if len(inners) < 2 {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("not enough inner instructions for protocol fee collection")
|
|
|
|
|
}
|
|
|
|
|
baseAmount := decimal.Zero
|
|
|
|
|
quoteAmount := decimal.Zero
|
|
|
|
|
var baseFound, quoteFound bool
|
|
|
|
|
for i, inner := range inners {
|
|
|
|
|
from, to, amount, err := parseTokenTransfer(tx.rawTx, inner)
|
|
|
|
|
if err != nil {
|
|
|
|
|
continue
|
|
|
|
|
//return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initial parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
|
|
|
|
|
}
|
|
|
|
|
if !baseFound && (from.Equals(tx.rawTx.accountList[vault0]) || to.Equals(tx.rawTx.accountList[vault0])) {
|
|
|
|
|
baseAmount = decimal.NewFromInt(int64(amount))
|
|
|
|
|
baseFound = true
|
|
|
|
|
} else if !quoteFound && (from.Equals(tx.rawTx.accountList[vault1]) || to.Equals(tx.rawTx.accountList[vault1])) {
|
|
|
|
|
quoteAmount = decimal.NewFromInt(int64(amount))
|
|
|
|
|
quoteFound = true
|
|
|
|
|
}
|
|
|
|
|
if (baseFound && quoteFound) || i >= 6 {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !baseFound && !quoteFound {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf(" collect fee v2 failed to find token transfer for both vaults in inner instructions")
|
|
|
|
|
}
|
|
|
|
|
offset[1] += 2
|
|
|
|
|
if baseAmount.Equal(decimal.Zero) && quoteAmount.Equal(decimal.Zero) {
|
|
|
|
|
return nil, offset, InstructionIgnoredError
|
|
|
|
|
}
|
|
|
|
|
if baseAmount.Equal(decimal.Zero) || quoteAmount.Equal(decimal.Zero) {
|
|
|
|
|
instructionName += "_on_side"
|
|
|
|
|
}
|
|
|
|
|
if (baseTokenBalance == nil && !baseAmount.Equal(decimal.Zero)) || (quoteTokenBalance == nil && !quoteAmount.Equal(decimal.Zero)) {
|
|
|
|
|
return nil, offset, fmt.Errorf("token balance is nil but amount is not zero")
|
|
|
|
|
}
|
|
|
|
|
return []Swap{
|
|
|
|
|
{
|
|
|
|
|
Program: SolProgramOrcaWhirPool,
|
|
|
|
|
Event: instructionName,
|
|
|
|
|
Pool: pool,
|
|
|
|
|
BaseMint: baseMint,
|
|
|
|
|
QuoteMint: quoteMint,
|
|
|
|
|
BaseTokenProgram: baseProgram,
|
|
|
|
|
QuoteTokenProgram: quoteProgram,
|
|
|
|
|
Creator: signer,
|
|
|
|
|
BaseMintDecimals: baseMintDecimals,
|
|
|
|
|
QuoteMintDecimals: quoteMintDecimals,
|
|
|
|
|
BaseAmount: baseAmount,
|
|
|
|
|
QuoteAmount: quoteAmount,
|
|
|
|
|
BaseReserve: baseReserve,
|
|
|
|
|
QuoteReserve: quoteReserve,
|
|
|
|
|
User: tx.rawTx.accountList[instruction.Accounts[0]],
|
|
|
|
|
EntryContract: entryContract,
|
|
|
|
|
},
|
|
|
|
|
}, offset, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func orcaWhirPoolCollectProtocolFeeV2Parser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
|
|
|
|
if len(instruction.Accounts) < 12 {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for initialize pool instruction")
|
|
|
|
|
}
|
|
|
|
|
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
|
|
|
|
signer := tx.rawTx.accountList[0]
|
|
|
|
|
pool := tx.rawTx.accountList[instruction.Accounts[1]]
|
|
|
|
|
baseMint := tx.rawTx.accountList[instruction.Accounts[3]]
|
|
|
|
|
quoteMint := tx.rawTx.accountList[instruction.Accounts[4]]
|
|
|
|
|
vault0 := instruction.Accounts[5]
|
|
|
|
|
vault1 := instruction.Accounts[6]
|
|
|
|
|
|
|
|
|
|
baseTokenBalance, _ := getTokenBalanceAfterTx(tx.rawTx, vault0)
|
|
|
|
|
quoteTokenBalance, _ := getTokenBalanceAfterTx(tx.rawTx, vault1)
|
|
|
|
|
if baseTokenBalance == nil && quoteTokenBalance == nil {
|
|
|
|
|
return nil, increaseOffset(offset), InstructionIgnoredError
|
|
|
|
|
}
|
|
|
|
|
var (
|
|
|
|
|
baseReserve decimal.Decimal
|
|
|
|
|
quoteReserve decimal.Decimal
|
|
|
|
|
|
|
|
|
|
baseMintDecimals uint8
|
|
|
|
|
quoteMintDecimals uint8
|
|
|
|
|
|
|
|
|
|
baseProgram solana.PublicKey
|
|
|
|
|
quoteProgram solana.PublicKey
|
|
|
|
|
)
|
|
|
|
|
if baseTokenBalance != nil {
|
|
|
|
|
baseReserve, _ = decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount)
|
|
|
|
|
baseMintDecimals = uint8(baseTokenBalance.UITokenAmount.Decimals)
|
|
|
|
|
baseProgram = baseTokenBalance.ProgramIDAccount
|
|
|
|
|
}
|
|
|
|
|
if quoteTokenBalance != nil {
|
|
|
|
|
quoteReserve, _ = decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount)
|
|
|
|
|
quoteMintDecimals = uint8(quoteTokenBalance.UITokenAmount.Decimals)
|
|
|
|
|
quoteProgram = quoteTokenBalance.ProgramIDAccount
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var instructionName = "remove_liquidity"
|
|
|
|
|
var prefixLen = offset[1]
|
|
|
|
|
inners, err := getInnerInstructions(innerInstructions, prefixLen)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("orca whirpool get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1])
|
|
|
|
|
}
|
|
|
|
|
if len(inners) < 2 {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("not enough inner instructions for protocol fee collection")
|
|
|
|
|
}
|
|
|
|
|
baseAmount := decimal.Zero
|
|
|
|
|
quoteAmount := decimal.Zero
|
|
|
|
|
var baseFound, quoteFound bool
|
|
|
|
|
for i, inner := range inners {
|
|
|
|
|
from, to, amount, err := parseTokenTransfer(tx.rawTx, inner)
|
|
|
|
|
if err != nil {
|
|
|
|
|
continue
|
|
|
|
|
// return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initial parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
|
|
|
|
|
}
|
|
|
|
|
if !baseFound && (from.Equals(tx.rawTx.accountList[vault0]) || to.Equals(tx.rawTx.accountList[vault0])) {
|
|
|
|
|
baseAmount = decimal.NewFromInt(int64(amount))
|
|
|
|
|
baseFound = true
|
|
|
|
|
} else if !quoteFound && (from.Equals(tx.rawTx.accountList[vault1]) || to.Equals(tx.rawTx.accountList[vault1])) {
|
|
|
|
|
quoteAmount = decimal.NewFromInt(int64(amount))
|
|
|
|
|
quoteFound = true
|
|
|
|
|
}
|
|
|
|
|
if (baseFound && quoteFound) || i >= 6 {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !baseFound && !quoteFound {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("collect protocol fee failed to find token transfer for both vaults in inner instructions")
|
|
|
|
|
}
|
|
|
|
|
offset[1] += 2
|
|
|
|
|
if baseAmount.Equal(decimal.Zero) && quoteAmount.Equal(decimal.Zero) {
|
|
|
|
|
return nil, offset, InstructionIgnoredError
|
|
|
|
|
}
|
|
|
|
|
if baseAmount.Equal(decimal.Zero) || quoteAmount.Equal(decimal.Zero) {
|
|
|
|
|
instructionName += "_on_side"
|
|
|
|
|
}
|
|
|
|
|
if (baseTokenBalance == nil && !baseAmount.Equal(decimal.Zero)) || (quoteTokenBalance == nil && !quoteAmount.Equal(decimal.Zero)) {
|
|
|
|
|
return nil, offset, fmt.Errorf("token balance is nil but amount is not zero")
|
|
|
|
|
}
|
|
|
|
|
return []Swap{
|
|
|
|
|
{
|
|
|
|
|
Program: SolProgramOrcaWhirPool,
|
|
|
|
|
Event: instructionName,
|
|
|
|
|
Pool: pool,
|
|
|
|
|
BaseMint: baseMint,
|
|
|
|
|
QuoteMint: quoteMint,
|
|
|
|
|
BaseTokenProgram: baseProgram,
|
|
|
|
|
QuoteTokenProgram: quoteProgram,
|
|
|
|
|
Creator: signer,
|
|
|
|
|
BaseMintDecimals: baseMintDecimals,
|
|
|
|
|
QuoteMintDecimals: quoteMintDecimals,
|
|
|
|
|
BaseAmount: baseAmount,
|
|
|
|
|
QuoteAmount: quoteAmount,
|
|
|
|
|
BaseReserve: baseReserve,
|
|
|
|
|
QuoteReserve: quoteReserve,
|
|
|
|
|
User: tx.rawTx.accountList[instruction.Accounts[0]],
|
|
|
|
|
EntryContract: entryContract,
|
|
|
|
|
},
|
|
|
|
|
}, offset, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func orcaWhirPoolSwapParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
|
|
|
|
if len(instruction.Accounts) < 11 {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for swap instruction")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
2026-04-16 14:24:14 +08:00
|
|
|
amount, otherAmountThreshold, amountSpecifiedIsInput, err := decodeOrcaWhirlpoolSwapArgs(instruction.Data)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), err
|
|
|
|
|
}
|
|
|
|
|
swapMode := SwapModeExactOut
|
|
|
|
|
if amountSpecifiedIsInput {
|
|
|
|
|
swapMode = SwapModeExactIn
|
|
|
|
|
}
|
2026-02-09 14:46:19 +08:00
|
|
|
|
|
|
|
|
user := tx.rawTx.accountList[instruction.Accounts[1]]
|
|
|
|
|
pool := tx.rawTx.accountList[instruction.Accounts[2]]
|
|
|
|
|
|
|
|
|
|
token0Account := tx.rawTx.accountList[instruction.Accounts[3]]
|
|
|
|
|
token1Account := tx.rawTx.accountList[instruction.Accounts[5]]
|
|
|
|
|
|
|
|
|
|
user0 := instruction.Accounts[3]
|
|
|
|
|
user1 := instruction.Accounts[5]
|
|
|
|
|
vault0 := instruction.Accounts[4]
|
|
|
|
|
vault1 := instruction.Accounts[6]
|
|
|
|
|
|
|
|
|
|
token0Account = tx.rawTx.accountList[user0]
|
|
|
|
|
token1Account = tx.rawTx.accountList[user1]
|
|
|
|
|
vault0Account := tx.rawTx.accountList[vault0]
|
|
|
|
|
vault1Account := tx.rawTx.accountList[vault1]
|
|
|
|
|
|
|
|
|
|
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault0)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("failed to get token0 vault balance after tx: %v", err)
|
|
|
|
|
}
|
|
|
|
|
quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault1)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("failed to get token1 vault balance after tx: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
baseReserve, _ := decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount)
|
|
|
|
|
quoteReserve, _ := decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount)
|
|
|
|
|
userBase := getAccountBalanceAfterTx(tx.rawTx, user0)
|
|
|
|
|
userQuote := getAccountBalanceAfterTx(tx.rawTx, user1)
|
|
|
|
|
|
|
|
|
|
var prefixLen = offset[1]
|
|
|
|
|
inners, err := getInnerInstructions(innerInstructions, prefixLen)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("orca whirpool swap get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1])
|
|
|
|
|
}
|
|
|
|
|
var baseAmount = decimal.Zero
|
|
|
|
|
var quoteAmount = decimal.Zero
|
|
|
|
|
var event string
|
|
|
|
|
var baseFound, quoteFound bool
|
|
|
|
|
for i, inner := range inners {
|
|
|
|
|
from, to, amount, err := parseTokenTransfer(tx.rawTx, inner)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("orca whirpool swap parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
|
|
|
|
|
}
|
|
|
|
|
if !baseFound && (from.Equals(vault0Account) || to.Equals(vault0Account)) {
|
|
|
|
|
baseAmount = decimal.NewFromInt(int64(amount))
|
|
|
|
|
if from.Equals(vault0Account) && to.Equals(token0Account) {
|
|
|
|
|
event = "buy"
|
|
|
|
|
} else if from.Equals(token0Account) && to.Equals(vault0Account) {
|
|
|
|
|
event = "sell"
|
|
|
|
|
}
|
|
|
|
|
baseFound = true
|
|
|
|
|
} else if !quoteFound && (from.Equals(vault1Account) || to.Equals(vault1Account)) {
|
|
|
|
|
quoteAmount = decimal.NewFromInt(int64(amount))
|
|
|
|
|
if from.Equals(vault1Account) && to.Equals(token1Account) {
|
|
|
|
|
event = "sell"
|
|
|
|
|
} else if from.Equals(token1Account) && to.Equals(vault1Account) {
|
|
|
|
|
event = "buy"
|
|
|
|
|
}
|
|
|
|
|
quoteFound = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if i >= 1 || (baseFound && quoteFound) {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
offset[1] += 2
|
|
|
|
|
if !baseFound || !quoteFound {
|
|
|
|
|
return nil, offset, fmt.Errorf("orca whirpool swap failed to find both base and quote token transfer in inner instructions")
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-16 14:24:14 +08:00
|
|
|
swap := Swap{
|
2026-02-09 14:46:19 +08:00
|
|
|
Program: SolProgramOrcaWhirPool,
|
|
|
|
|
Event: event,
|
|
|
|
|
Pool: pool,
|
|
|
|
|
BaseMint: baseTokenBalance.MintAccount,
|
|
|
|
|
QuoteMint: quoteTokenBalance.MintAccount,
|
|
|
|
|
BaseTokenProgram: baseTokenBalance.ProgramIDAccount,
|
|
|
|
|
QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount,
|
|
|
|
|
BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals),
|
|
|
|
|
QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals),
|
|
|
|
|
BaseAmount: baseAmount,
|
|
|
|
|
QuoteAmount: quoteAmount,
|
|
|
|
|
BaseReserve: baseReserve,
|
|
|
|
|
QuoteReserve: quoteReserve,
|
|
|
|
|
UserBaseBalance: userBase,
|
|
|
|
|
UserQuoteBalance: userQuote,
|
|
|
|
|
User: user,
|
|
|
|
|
EntryContract: entryContract,
|
2026-04-16 14:24:14 +08:00
|
|
|
}
|
|
|
|
|
swap.SetSwapAmountInfo(swapMode, decimal.NewFromUint64(amount), decimal.NewFromUint64(otherAmountThreshold))
|
|
|
|
|
|
|
|
|
|
return []Swap{swap}, offset, nil
|
2026-02-09 14:46:19 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func orcaWhirPoolSwapV2Parser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
|
|
|
|
if len(instruction.Accounts) < 15 {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for swap instruction")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
2026-04-16 14:24:14 +08:00
|
|
|
amount, otherAmountThreshold, amountSpecifiedIsInput, err := decodeOrcaWhirlpoolSwapArgs(instruction.Data)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), err
|
|
|
|
|
}
|
|
|
|
|
swapMode := SwapModeExactOut
|
|
|
|
|
if amountSpecifiedIsInput {
|
|
|
|
|
swapMode = SwapModeExactIn
|
|
|
|
|
}
|
2026-02-09 14:46:19 +08:00
|
|
|
|
|
|
|
|
user := tx.rawTx.accountList[instruction.Accounts[3]]
|
|
|
|
|
pool := tx.rawTx.accountList[instruction.Accounts[4]]
|
|
|
|
|
|
|
|
|
|
user0 := instruction.Accounts[7]
|
|
|
|
|
user1 := instruction.Accounts[9]
|
|
|
|
|
vault0 := instruction.Accounts[8]
|
|
|
|
|
vault1 := instruction.Accounts[10]
|
|
|
|
|
|
|
|
|
|
token0Account := tx.rawTx.accountList[user0]
|
|
|
|
|
token1Account := tx.rawTx.accountList[user1]
|
|
|
|
|
vault0Account := tx.rawTx.accountList[vault0]
|
|
|
|
|
vault1Account := tx.rawTx.accountList[vault1]
|
|
|
|
|
|
|
|
|
|
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault0)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("failed to get token0 vault balance after tx: %v", err)
|
|
|
|
|
}
|
|
|
|
|
quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault1)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("failed to get token1 vault balance after tx: %v", err)
|
|
|
|
|
}
|
|
|
|
|
baseReserve, _ := decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount)
|
|
|
|
|
quoteReserve, _ := decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount)
|
|
|
|
|
userBase := getAccountBalanceAfterTx(tx.rawTx, user0)
|
|
|
|
|
userQuote := getAccountBalanceAfterTx(tx.rawTx, user1)
|
|
|
|
|
|
|
|
|
|
var prefixLen = offset[1]
|
|
|
|
|
inners, err := getInnerInstructions(innerInstructions, prefixLen)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("orca whirpool swap get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1])
|
|
|
|
|
}
|
|
|
|
|
var baseAmount = decimal.Zero
|
|
|
|
|
var quoteAmount = decimal.Zero
|
|
|
|
|
var event string
|
|
|
|
|
var baseFound, quoteFound bool
|
|
|
|
|
var skipOffset = 0
|
|
|
|
|
for i, inner := range inners {
|
2026-02-11 14:58:12 +08:00
|
|
|
if !tx.rawTx.accountList[inner.ProgramIDIndex].Equals(solana.Token2022ProgramID) && !tx.rawTx.accountList[inner.ProgramIDIndex].Equals(solana.TokenProgramID) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2026-02-09 14:46:19 +08:00
|
|
|
from, to, amount, err := parseTokenTransfer(tx.rawTx, inner)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("orca whirpool swapv2 parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
|
|
|
|
|
}
|
|
|
|
|
if !baseFound && (from.Equals(vault0Account) || to.Equals(vault0Account)) {
|
|
|
|
|
baseAmount = decimal.NewFromInt(int64(amount))
|
|
|
|
|
if from.Equals(vault0Account) && to.Equals(token0Account) {
|
|
|
|
|
event = "buy"
|
|
|
|
|
} else if from.Equals(token0Account) && to.Equals(vault0Account) {
|
|
|
|
|
event = "sell"
|
|
|
|
|
}
|
|
|
|
|
baseFound = true
|
|
|
|
|
} else if !quoteFound && (from.Equals(vault1Account) || to.Equals(vault1Account)) {
|
|
|
|
|
quoteAmount = decimal.NewFromInt(int64(amount))
|
|
|
|
|
if from.Equals(vault1Account) && to.Equals(token1Account) {
|
|
|
|
|
event = "sell"
|
|
|
|
|
} else if from.Equals(token1Account) && to.Equals(vault1Account) {
|
|
|
|
|
event = "buy"
|
|
|
|
|
}
|
|
|
|
|
quoteFound = true
|
|
|
|
|
}
|
|
|
|
|
if baseFound && quoteFound {
|
|
|
|
|
skipOffset = i
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !baseFound || !quoteFound {
|
|
|
|
|
return nil, offset, fmt.Errorf("orca whirpool swapV2 failed to find both base and quote token transfer in inner instructions")
|
|
|
|
|
}
|
|
|
|
|
offset[1] += uint(skipOffset + 1)
|
|
|
|
|
|
2026-04-16 14:24:14 +08:00
|
|
|
swap := Swap{
|
2026-02-09 14:46:19 +08:00
|
|
|
Program: SolProgramOrcaWhirPool,
|
|
|
|
|
Event: event,
|
|
|
|
|
Pool: pool,
|
|
|
|
|
BaseMint: baseTokenBalance.MintAccount,
|
|
|
|
|
QuoteMint: quoteTokenBalance.MintAccount,
|
|
|
|
|
BaseTokenProgram: baseTokenBalance.ProgramIDAccount,
|
|
|
|
|
QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount,
|
|
|
|
|
BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals),
|
|
|
|
|
QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals),
|
|
|
|
|
BaseAmount: baseAmount,
|
|
|
|
|
QuoteAmount: quoteAmount,
|
|
|
|
|
BaseReserve: baseReserve,
|
|
|
|
|
QuoteReserve: quoteReserve,
|
|
|
|
|
UserBaseBalance: userBase,
|
|
|
|
|
UserQuoteBalance: userQuote,
|
|
|
|
|
User: user,
|
|
|
|
|
EntryContract: entryContract,
|
2026-04-16 14:24:14 +08:00
|
|
|
}
|
|
|
|
|
swap.SetSwapAmountInfo(swapMode, decimal.NewFromUint64(amount), decimal.NewFromUint64(otherAmountThreshold))
|
|
|
|
|
|
|
|
|
|
return []Swap{swap}, offset, nil
|
2026-02-09 14:46:19 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func orcaWhirPoolTwoHopSwapParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
|
|
|
|
if len(instruction.Accounts) < 12 {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for twoHopSwap instruction")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
2026-04-16 14:24:14 +08:00
|
|
|
amountSpecified, otherAmountThreshold, amountSpecifiedIsInput, err := decodeOrcaWhirlpoolTwoHopSwapArgs(instruction.Data)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), err
|
|
|
|
|
}
|
|
|
|
|
swapMode := SwapModeExactOut
|
|
|
|
|
if amountSpecifiedIsInput {
|
|
|
|
|
swapMode = SwapModeExactIn
|
|
|
|
|
}
|
2026-02-09 14:46:19 +08:00
|
|
|
|
|
|
|
|
user := tx.rawTx.accountList[instruction.Accounts[1]]
|
|
|
|
|
pool1 := tx.rawTx.accountList[instruction.Accounts[2]]
|
|
|
|
|
pool2 := tx.rawTx.accountList[instruction.Accounts[3]]
|
|
|
|
|
|
|
|
|
|
pool1UserBase := instruction.Accounts[4]
|
|
|
|
|
pool1VaultBase := instruction.Accounts[5]
|
|
|
|
|
|
|
|
|
|
pool1UserQuote := instruction.Accounts[6]
|
|
|
|
|
pool1VaultQuote := instruction.Accounts[7]
|
|
|
|
|
|
|
|
|
|
pool2UserBase := instruction.Accounts[8]
|
|
|
|
|
pool2VaultBase := instruction.Accounts[9]
|
|
|
|
|
|
|
|
|
|
pool2UserQuote := instruction.Accounts[10]
|
|
|
|
|
pool2VaultQuote := instruction.Accounts[11]
|
|
|
|
|
|
|
|
|
|
swaps := make([]Swap, 2)
|
|
|
|
|
{
|
|
|
|
|
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, pool1VaultBase)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("failed to get pool1 token0 vault balance after tx: %v", err)
|
|
|
|
|
}
|
|
|
|
|
quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, pool1VaultQuote)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("failed to get pool1 token1 vault balance after tx: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
userBase := getAccountBalanceAfterTx(tx.rawTx, pool1UserBase)
|
|
|
|
|
userQuote := getAccountBalanceAfterTx(tx.rawTx, pool1UserQuote)
|
|
|
|
|
|
|
|
|
|
baseReserve, _ := decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount)
|
|
|
|
|
quoteReserve, _ := decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount)
|
|
|
|
|
|
|
|
|
|
var baseAmount = decimal.Zero
|
|
|
|
|
var quoteAmount = decimal.Zero
|
|
|
|
|
var event string
|
|
|
|
|
inners, err := getInnerInstructions(innerInstructions, offset[1])
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("orca whirpool two hop swap get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1])
|
|
|
|
|
}
|
|
|
|
|
var nextInstructionIndex = uint(0)
|
|
|
|
|
var baseFound, quoteFound bool
|
|
|
|
|
for i, inner := range inners {
|
|
|
|
|
if !tx.rawTx.accountList[inner.ProgramIDIndex].Equals(solana.Token2022ProgramID) && !tx.rawTx.accountList[inner.ProgramIDIndex].Equals(solana.TokenProgramID) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
from, to, amount, err := parseTokenTransfer(tx.rawTx, inner)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("orca whirpool two hop swap parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
|
|
|
|
|
}
|
|
|
|
|
if from.Equals(tx.rawTx.accountList[pool1VaultBase]) || to.Equals(tx.rawTx.accountList[pool1VaultBase]) {
|
|
|
|
|
baseAmount = decimal.NewFromInt(int64(amount))
|
|
|
|
|
if from.Equals(tx.rawTx.accountList[pool1VaultBase]) && to.Equals(tx.rawTx.accountList[pool1UserBase]) {
|
|
|
|
|
event = "buy"
|
|
|
|
|
} else if from.Equals(tx.rawTx.accountList[pool1UserBase]) && to.Equals(tx.rawTx.accountList[pool1VaultBase]) {
|
|
|
|
|
event = "sell"
|
|
|
|
|
}
|
|
|
|
|
baseFound = true
|
|
|
|
|
} else if from.Equals(tx.rawTx.accountList[pool1VaultQuote]) || to.Equals(tx.rawTx.accountList[pool1VaultQuote]) {
|
|
|
|
|
quoteAmount = decimal.NewFromInt(int64(amount))
|
|
|
|
|
if from.Equals(tx.rawTx.accountList[pool1VaultQuote]) && to.Equals(tx.rawTx.accountList[pool1UserQuote]) {
|
|
|
|
|
event = "sell"
|
|
|
|
|
} else if from.Equals(tx.rawTx.accountList[pool1UserQuote]) && to.Equals(tx.rawTx.accountList[pool1VaultQuote]) {
|
|
|
|
|
event = "buy"
|
|
|
|
|
}
|
|
|
|
|
quoteFound = true
|
|
|
|
|
}
|
|
|
|
|
nextInstructionIndex = uint(i + 1)
|
|
|
|
|
if baseFound && quoteFound {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
offset[1] += nextInstructionIndex
|
|
|
|
|
swaps[0] = Swap{
|
|
|
|
|
Program: SolProgramOrcaWhirPool,
|
|
|
|
|
Event: event,
|
|
|
|
|
Pool: pool1,
|
|
|
|
|
BaseMint: baseTokenBalance.MintAccount,
|
|
|
|
|
QuoteMint: quoteTokenBalance.MintAccount,
|
|
|
|
|
BaseTokenProgram: baseTokenBalance.ProgramIDAccount,
|
|
|
|
|
QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount,
|
|
|
|
|
BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals),
|
|
|
|
|
QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals),
|
|
|
|
|
BaseAmount: baseAmount,
|
|
|
|
|
QuoteAmount: quoteAmount,
|
|
|
|
|
BaseReserve: baseReserve,
|
|
|
|
|
QuoteReserve: quoteReserve,
|
|
|
|
|
UserBaseBalance: userBase,
|
|
|
|
|
UserQuoteBalance: userQuote,
|
|
|
|
|
User: user,
|
|
|
|
|
EntryContract: entryContract,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, pool2VaultBase)
|
|
|
|
|
if err != nil {
|
2026-02-11 14:58:12 +08:00
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("failed to get pool2 token0 vault balance after tx: %v", err)
|
2026-02-09 14:46:19 +08:00
|
|
|
}
|
|
|
|
|
quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, pool2VaultQuote)
|
|
|
|
|
if err != nil {
|
2026-02-11 14:58:12 +08:00
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("failed to get pool2 token1 vault balance after tx: %v", err)
|
2026-02-09 14:46:19 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
userBase := getAccountBalanceAfterTx(tx.rawTx, pool2UserBase)
|
|
|
|
|
userQuote := getAccountBalanceAfterTx(tx.rawTx, pool2UserQuote)
|
|
|
|
|
|
|
|
|
|
baseReserve, _ := decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount)
|
|
|
|
|
quoteReserve, _ := decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount)
|
|
|
|
|
var baseAmount = decimal.Zero
|
|
|
|
|
var quoteAmount = decimal.Zero
|
|
|
|
|
var event string
|
|
|
|
|
inners, err := getInnerInstructions(innerInstructions, offset[1])
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("orca whirpool two hop swap get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1])
|
|
|
|
|
}
|
|
|
|
|
var baseFound, quoteFound bool
|
|
|
|
|
var nextInstructionIndex = uint(0)
|
|
|
|
|
for i, inner := range inners {
|
|
|
|
|
if !tx.rawTx.accountList[inner.ProgramIDIndex].Equals(solana.Token2022ProgramID) && !tx.rawTx.accountList[inner.ProgramIDIndex].Equals(solana.TokenProgramID) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
from, to, amount, err := parseTokenTransfer(tx.rawTx, inner)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("orca whirpool two hop swap parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
|
|
|
|
|
}
|
|
|
|
|
if from.Equals(tx.rawTx.accountList[pool2VaultBase]) || to.Equals(tx.rawTx.accountList[pool2VaultBase]) {
|
|
|
|
|
baseAmount = decimal.NewFromInt(int64(amount))
|
|
|
|
|
if from.Equals(tx.rawTx.accountList[pool2VaultBase]) && to.Equals(tx.rawTx.accountList[pool2UserBase]) {
|
|
|
|
|
event = "buy"
|
|
|
|
|
} else if from.Equals(tx.rawTx.accountList[pool2UserBase]) && to.Equals(tx.rawTx.accountList[pool2VaultBase]) {
|
|
|
|
|
event = "sell"
|
|
|
|
|
}
|
|
|
|
|
baseFound = true
|
|
|
|
|
} else if from.Equals(tx.rawTx.accountList[pool2VaultQuote]) || to.Equals(tx.rawTx.accountList[pool2VaultQuote]) {
|
|
|
|
|
quoteAmount = decimal.NewFromInt(int64(amount))
|
|
|
|
|
if from.Equals(tx.rawTx.accountList[pool2VaultQuote]) && to.Equals(tx.rawTx.accountList[pool2UserQuote]) {
|
|
|
|
|
event = "sell"
|
|
|
|
|
} else if from.Equals(tx.rawTx.accountList[pool2UserQuote]) && to.Equals(tx.rawTx.accountList[pool2VaultQuote]) {
|
|
|
|
|
event = "buy"
|
|
|
|
|
}
|
|
|
|
|
quoteFound = true
|
|
|
|
|
}
|
|
|
|
|
nextInstructionIndex = uint(i + 1)
|
|
|
|
|
if baseFound && quoteFound {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
offset[1] += nextInstructionIndex
|
|
|
|
|
swaps[1] = Swap{
|
|
|
|
|
Program: SolProgramOrcaWhirPool,
|
|
|
|
|
Event: event,
|
|
|
|
|
Pool: pool2,
|
|
|
|
|
BaseMint: baseTokenBalance.MintAccount,
|
|
|
|
|
QuoteMint: quoteTokenBalance.MintAccount,
|
|
|
|
|
BaseTokenProgram: baseTokenBalance.ProgramIDAccount,
|
|
|
|
|
QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount,
|
|
|
|
|
BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals),
|
|
|
|
|
QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals),
|
|
|
|
|
BaseAmount: baseAmount,
|
|
|
|
|
QuoteAmount: quoteAmount,
|
|
|
|
|
BaseReserve: baseReserve,
|
|
|
|
|
QuoteReserve: quoteReserve,
|
|
|
|
|
UserBaseBalance: userBase,
|
|
|
|
|
UserQuoteBalance: userQuote,
|
|
|
|
|
User: user,
|
|
|
|
|
EntryContract: entryContract,
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-16 14:24:14 +08:00
|
|
|
fixedSide := fixedSwapAmountSide(swaps[0].Event, swapMode)
|
|
|
|
|
fixedMint := swapMintForSide(swaps[0].BaseMint, swaps[0].QuoteMint, fixedSide)
|
|
|
|
|
limitSide := oppositeSwapAmountSide(fixedSide)
|
|
|
|
|
limitMint := swapMintForSide(swaps[1].BaseMint, swaps[1].QuoteMint, limitSide)
|
|
|
|
|
actualLimitAmount := swapAmountForSide(swaps[1].BaseAmount, swaps[1].QuoteAmount, limitSide)
|
|
|
|
|
if swapMode == SwapModeExactOut {
|
|
|
|
|
fixedSide = fixedSwapAmountSide(swaps[1].Event, swapMode)
|
|
|
|
|
fixedMint = swapMintForSide(swaps[1].BaseMint, swaps[1].QuoteMint, fixedSide)
|
|
|
|
|
limitSide = oppositeSwapAmountSide(fixedSwapAmountSide(swaps[0].Event, swapMode))
|
|
|
|
|
limitMint = swapMintForSide(swaps[0].BaseMint, swaps[0].QuoteMint, limitSide)
|
|
|
|
|
actualLimitAmount = swapAmountForSide(swaps[0].BaseAmount, swaps[0].QuoteAmount, limitSide)
|
|
|
|
|
}
|
|
|
|
|
swaps[0].SetSwapAmountInfoDetailed(
|
|
|
|
|
swapMode,
|
|
|
|
|
decimal.NewFromUint64(amountSpecified),
|
|
|
|
|
fixedSide,
|
|
|
|
|
fixedMint,
|
|
|
|
|
limitSwapAmountType(swapMode),
|
|
|
|
|
decimal.NewFromUint64(otherAmountThreshold),
|
|
|
|
|
limitSide,
|
|
|
|
|
limitMint,
|
|
|
|
|
actualLimitAmount,
|
|
|
|
|
)
|
2026-02-09 14:46:19 +08:00
|
|
|
return swaps, offset, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func orcaWhirPoolTwoHopSwapV2Parser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
|
|
|
|
if len(instruction.Accounts) < 15 {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for twoHopSwapV2 instruction")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
2026-04-16 14:24:14 +08:00
|
|
|
amountSpecified, otherAmountThreshold, amountSpecifiedIsInput, err := decodeOrcaWhirlpoolTwoHopSwapArgs(instruction.Data)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), err
|
|
|
|
|
}
|
|
|
|
|
swapMode := SwapModeExactOut
|
|
|
|
|
if amountSpecifiedIsInput {
|
|
|
|
|
swapMode = SwapModeExactIn
|
|
|
|
|
}
|
2026-02-09 14:46:19 +08:00
|
|
|
|
|
|
|
|
user := tx.rawTx.accountList[instruction.Accounts[14]]
|
|
|
|
|
pool1 := tx.rawTx.accountList[instruction.Accounts[0]]
|
|
|
|
|
pool2 := tx.rawTx.accountList[instruction.Accounts[1]]
|
|
|
|
|
|
|
|
|
|
pool1UserBase := instruction.Accounts[8]
|
|
|
|
|
pool1VaultBase := instruction.Accounts[9]
|
|
|
|
|
|
|
|
|
|
//pool1UserQuote := instruction.Accounts[6]
|
|
|
|
|
pool1VaultQuote := instruction.Accounts[10]
|
|
|
|
|
|
|
|
|
|
//pool2UserBase := instruction.Accounts[8]
|
|
|
|
|
pool2VaultBase := instruction.Accounts[11]
|
|
|
|
|
|
2026-02-11 14:58:12 +08:00
|
|
|
pool2VaultQuote := instruction.Accounts[12]
|
|
|
|
|
pool2UserQuote := instruction.Accounts[13]
|
2026-02-09 14:46:19 +08:00
|
|
|
|
|
|
|
|
swaps := make([]Swap, 2)
|
|
|
|
|
{
|
|
|
|
|
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, pool1VaultBase)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("failed to get pool1 token0 vault balance after tx: %v", err)
|
|
|
|
|
}
|
|
|
|
|
quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, pool1VaultQuote)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("failed to get pool1 token1 vault balance after tx: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
userBase := getAccountBalanceAfterTx(tx.rawTx, pool1UserBase)
|
|
|
|
|
userQuote := decimal.Zero
|
|
|
|
|
|
|
|
|
|
baseReserve, _ := decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount)
|
|
|
|
|
quoteReserve, _ := decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount)
|
|
|
|
|
|
|
|
|
|
var baseAmount = decimal.Zero
|
|
|
|
|
var quoteAmount = decimal.Zero
|
|
|
|
|
var event string
|
|
|
|
|
inners, err := getInnerInstructions(innerInstructions, offset[1])
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("orca whirpool two hop swap get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1])
|
|
|
|
|
}
|
|
|
|
|
var baseFound, quoteFound bool
|
|
|
|
|
for _, inner := range inners {
|
|
|
|
|
if !tx.rawTx.accountList[inner.ProgramIDIndex].Equals(solana.Token2022ProgramID) && !tx.rawTx.accountList[inner.ProgramIDIndex].Equals(solana.TokenProgramID) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
from, to, amount, err := parseTokenTransfer(tx.rawTx, inner)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("orca whirpool two hop swap parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
|
|
|
|
|
}
|
|
|
|
|
if from.Equals(tx.rawTx.accountList[pool1VaultBase]) || to.Equals(tx.rawTx.accountList[pool1VaultBase]) {
|
|
|
|
|
baseAmount = decimal.NewFromInt(int64(amount))
|
|
|
|
|
if from.Equals(tx.rawTx.accountList[pool1VaultBase]) && to.Equals(tx.rawTx.accountList[pool1UserBase]) {
|
|
|
|
|
event = "buy"
|
|
|
|
|
} else if from.Equals(tx.rawTx.accountList[pool1UserBase]) && to.Equals(tx.rawTx.accountList[pool1VaultBase]) {
|
|
|
|
|
event = "sell"
|
|
|
|
|
}
|
|
|
|
|
baseFound = true
|
|
|
|
|
} else if from.Equals(tx.rawTx.accountList[pool1VaultQuote]) || to.Equals(tx.rawTx.accountList[pool1VaultQuote]) {
|
|
|
|
|
quoteAmount = decimal.NewFromInt(int64(amount))
|
|
|
|
|
if from.Equals(tx.rawTx.accountList[pool1VaultQuote]) && to.Equals(tx.rawTx.accountList[pool2VaultBase]) {
|
|
|
|
|
event = "sell"
|
|
|
|
|
} else if from.Equals(tx.rawTx.accountList[pool2VaultBase]) && to.Equals(tx.rawTx.accountList[pool1VaultQuote]) {
|
|
|
|
|
event = "buy"
|
|
|
|
|
}
|
|
|
|
|
quoteFound = true
|
|
|
|
|
}
|
|
|
|
|
if baseFound && quoteFound {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
swaps[0] = Swap{
|
|
|
|
|
Program: SolProgramOrcaWhirPool,
|
|
|
|
|
Event: event,
|
|
|
|
|
Pool: pool1,
|
|
|
|
|
BaseMint: baseTokenBalance.MintAccount,
|
|
|
|
|
QuoteMint: quoteTokenBalance.MintAccount,
|
|
|
|
|
BaseTokenProgram: baseTokenBalance.ProgramIDAccount,
|
|
|
|
|
QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount,
|
|
|
|
|
BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals),
|
|
|
|
|
QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals),
|
|
|
|
|
BaseAmount: baseAmount,
|
|
|
|
|
QuoteAmount: quoteAmount,
|
|
|
|
|
BaseReserve: baseReserve,
|
|
|
|
|
QuoteReserve: quoteReserve,
|
|
|
|
|
UserBaseBalance: userBase,
|
|
|
|
|
UserQuoteBalance: userQuote,
|
|
|
|
|
User: user,
|
|
|
|
|
EntryContract: entryContract,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, pool2VaultBase)
|
|
|
|
|
if err != nil {
|
2026-02-11 14:58:12 +08:00
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("failed to get pool2 token0 vault balance after tx: %v", err)
|
2026-02-09 14:46:19 +08:00
|
|
|
}
|
|
|
|
|
quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, pool2VaultQuote)
|
|
|
|
|
if err != nil {
|
2026-02-11 14:58:12 +08:00
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("failed to get pool2 token1 vault balance after tx: %v", err)
|
2026-02-09 14:46:19 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
userBase := decimal.Zero
|
|
|
|
|
userQuote := getAccountBalanceAfterTx(tx.rawTx, pool2UserQuote)
|
|
|
|
|
|
|
|
|
|
baseReserve, _ := decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount)
|
|
|
|
|
quoteReserve, _ := decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount)
|
|
|
|
|
var baseAmount = decimal.Zero
|
|
|
|
|
var quoteAmount = decimal.Zero
|
|
|
|
|
var event string
|
|
|
|
|
inners, err := getInnerInstructions(innerInstructions, offset[1])
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("orca whirpool two hop swap get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1])
|
|
|
|
|
}
|
|
|
|
|
var baseFound, quoteFound bool
|
|
|
|
|
var nextInstructionIndex = uint(0)
|
|
|
|
|
for i, inner := range inners {
|
|
|
|
|
if !tx.rawTx.accountList[inner.ProgramIDIndex].Equals(solana.Token2022ProgramID) && !tx.rawTx.accountList[inner.ProgramIDIndex].Equals(solana.TokenProgramID) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
from, to, amount, err := parseTokenTransfer(tx.rawTx, inner)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, increaseOffset(offset), fmt.Errorf("orca whirpool two hop swap parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
|
|
|
|
|
}
|
|
|
|
|
if from.Equals(tx.rawTx.accountList[pool2VaultBase]) || to.Equals(tx.rawTx.accountList[pool2VaultBase]) {
|
|
|
|
|
baseAmount = decimal.NewFromInt(int64(amount))
|
|
|
|
|
if from.Equals(tx.rawTx.accountList[pool2VaultBase]) && to.Equals(tx.rawTx.accountList[pool1VaultBase]) {
|
|
|
|
|
event = "buy"
|
|
|
|
|
} else if from.Equals(tx.rawTx.accountList[pool1VaultBase]) && to.Equals(tx.rawTx.accountList[pool2VaultBase]) {
|
|
|
|
|
event = "sell"
|
|
|
|
|
}
|
|
|
|
|
baseFound = true
|
|
|
|
|
} else if from.Equals(tx.rawTx.accountList[pool2VaultQuote]) || to.Equals(tx.rawTx.accountList[pool2VaultQuote]) {
|
|
|
|
|
quoteAmount = decimal.NewFromInt(int64(amount))
|
|
|
|
|
if from.Equals(tx.rawTx.accountList[pool2VaultQuote]) && to.Equals(tx.rawTx.accountList[pool2UserQuote]) {
|
|
|
|
|
event = "sell"
|
|
|
|
|
} else if from.Equals(tx.rawTx.accountList[pool2UserQuote]) && to.Equals(tx.rawTx.accountList[pool2VaultQuote]) {
|
|
|
|
|
event = "buy"
|
|
|
|
|
}
|
|
|
|
|
quoteFound = true
|
|
|
|
|
}
|
|
|
|
|
nextInstructionIndex = uint(i + 1)
|
|
|
|
|
if baseFound && quoteFound {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
offset[1] += nextInstructionIndex
|
|
|
|
|
swaps[1] = Swap{
|
|
|
|
|
Program: SolProgramOrcaWhirPool,
|
|
|
|
|
Event: event,
|
|
|
|
|
Pool: pool2,
|
|
|
|
|
BaseMint: baseTokenBalance.MintAccount,
|
|
|
|
|
QuoteMint: quoteTokenBalance.MintAccount,
|
|
|
|
|
BaseTokenProgram: baseTokenBalance.ProgramIDAccount,
|
|
|
|
|
QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount,
|
|
|
|
|
BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals),
|
|
|
|
|
QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals),
|
|
|
|
|
BaseAmount: baseAmount,
|
|
|
|
|
QuoteAmount: quoteAmount,
|
|
|
|
|
BaseReserve: baseReserve,
|
|
|
|
|
QuoteReserve: quoteReserve,
|
|
|
|
|
UserBaseBalance: userBase,
|
|
|
|
|
UserQuoteBalance: userQuote,
|
|
|
|
|
User: user,
|
|
|
|
|
EntryContract: entryContract,
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-16 14:24:14 +08:00
|
|
|
fixedSide := fixedSwapAmountSide(swaps[0].Event, swapMode)
|
|
|
|
|
fixedMint := swapMintForSide(swaps[0].BaseMint, swaps[0].QuoteMint, fixedSide)
|
|
|
|
|
limitSide := oppositeSwapAmountSide(fixedSide)
|
|
|
|
|
limitMint := swapMintForSide(swaps[1].BaseMint, swaps[1].QuoteMint, limitSide)
|
|
|
|
|
actualLimitAmount := swapAmountForSide(swaps[1].BaseAmount, swaps[1].QuoteAmount, limitSide)
|
|
|
|
|
if swapMode == SwapModeExactOut {
|
|
|
|
|
fixedSide = fixedSwapAmountSide(swaps[1].Event, swapMode)
|
|
|
|
|
fixedMint = swapMintForSide(swaps[1].BaseMint, swaps[1].QuoteMint, fixedSide)
|
|
|
|
|
limitSide = oppositeSwapAmountSide(fixedSwapAmountSide(swaps[0].Event, swapMode))
|
|
|
|
|
limitMint = swapMintForSide(swaps[0].BaseMint, swaps[0].QuoteMint, limitSide)
|
|
|
|
|
actualLimitAmount = swapAmountForSide(swaps[0].BaseAmount, swaps[0].QuoteAmount, limitSide)
|
|
|
|
|
}
|
|
|
|
|
swaps[0].SetSwapAmountInfoDetailed(
|
|
|
|
|
swapMode,
|
|
|
|
|
decimal.NewFromUint64(amountSpecified),
|
|
|
|
|
fixedSide,
|
|
|
|
|
fixedMint,
|
|
|
|
|
limitSwapAmountType(swapMode),
|
|
|
|
|
decimal.NewFromUint64(otherAmountThreshold),
|
|
|
|
|
limitSide,
|
|
|
|
|
limitMint,
|
|
|
|
|
actualLimitAmount,
|
|
|
|
|
)
|
2026-02-09 14:46:19 +08:00
|
|
|
return swaps, offset, nil
|
|
|
|
|
}
|