Files
pump-parser/orcawhirpool.go

1366 lines
55 KiB
Go
Raw Permalink Normal View History

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) {
2026-04-20 15:25:08 +08:00
instructionName += "_one_side"
2026-02-09 14:46:19 +08:00
}
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) {
2026-04-20 15:25:08 +08:00
instructionName += "_one_side"
2026-02-09 14:46:19 +08:00
}
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) {
2026-04-20 15:25:08 +08:00
instructionName += "_one_side"
2026-02-09 14:46:19 +08:00
}
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) {
2026-04-20 15:25:08 +08:00
instructionName += "_one_side"
2026-02-09 14:46:19 +08:00
}
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) {
2026-04-20 15:25:08 +08:00
instructionName += "_one_side"
2026-02-09 14:46:19 +08:00
}
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-04-20 15:25:08 +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-04-20 15:25:08 +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-04-20 16:31:18 +08:00
swaps[0].SlippageBps = decimal.Zero
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-04-20 16:31:18 +08:00
swaps[0].SlippageBps = decimal.Zero
2026-02-09 14:46:19 +08:00
return swaps, offset, nil
}