Files
pump-parser/raydiumlaunchlab.go

502 lines
18 KiB
Go
Raw Normal View History

2026-02-09 14:46:19 +08:00
package pump_parser
import (
"bytes"
"fmt"
agbinary "github.com/gagliardetto/binary"
"github.com/gagliardetto/solana-go"
"github.com/shopspring/decimal"
)
func raydiumLaunchLabParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if !tx.rawTx.accountList[instruction.ProgramIDIndex].Equals(raydiumLaunchLabProgramID) {
return nil, increaseOffset(offset), fmt.Errorf("raydiumLaunchLab instruction not found, offset, %d, %d", offset[0], offset[1])
}
decode := instruction.Data
if len(decode) < 8 {
return nil, increaseOffset(offset), fmt.Errorf("raydiumLaunchLab program instruction data too short, offset, %d, %d", offset[0], offset[1])
}
discriminator := *(*[8]byte)(decode[:8])
switch discriminator {
case raydiumLaunchLabInitializeWithToken2022PoolDiscriminator, raydiumLaunchLabInitializeV2PoolDiscriminator:
return raydiumLaunchLabInitializeParser(tx, instruction, innerInstructions, offset)
case raydiumLaunchLabMigrateToAmmDiscriminator:
return raydiumLaunchLabMigrateToAmmParser(tx, instruction, innerInstructions, offset)
case raydiumLaunchLabMigrateToCpmmDiscriminator:
return raydiumLaunchLabMigrateToCpmmParser(tx, instruction, innerInstructions, offset)
case raydiumLaunchLabSellExactInDiscriminator,
raydiumLaunchLabSellExactOutDiscriminator,
raydiumLaunchLabBuyExactInDiscriminator,
raydiumLaunchLabBuyExactOutDiscriminator:
return raydiumLaunchLabSwapParser(tx, instruction, innerInstructions, offset)
default:
return nil, increaseOffset(offset), InstructionIgnoredError
}
}
type VestingParam struct {
TotalLockedAmount uint64
CliffPeriod uint64
UnlockPeriod uint64
}
type CurveParamKind uint8
const (
CurveParamConstant CurveParamKind = 0
CurveParamFixed CurveParamKind = 1
CurveParamLinear CurveParamKind = 2
)
type CurveParam struct {
// rust enum ConstantCurve/FixedCurve/LinearCurve
Kind CurveParamKind
Constant *ConstantCurve
Fixed *FixedCurve
Linear *LinearCurve
}
func (c *CurveParam) TotalSupply() uint64 {
switch c.Kind {
case CurveParamConstant:
return c.Constant.TotalSupply
case CurveParamFixed:
return c.Fixed.TotalSupply
case CurveParamLinear:
return c.Linear.TotalSupply
default:
return 0
}
}
// UnmarshalWithDecoder 让 agbinary/borsh 解码时走自定义逻辑
func (c *CurveParam) UnmarshalWithDecoder(dec *agbinary.Decoder) error {
var tag uint8
if err := dec.Decode(&tag); err != nil {
return fmt.Errorf("decode CurveParam tag: %w", err)
}
c.Kind = CurveParamKind(tag)
c.Constant, c.Fixed, c.Linear = nil, nil, nil
switch c.Kind {
case CurveParamConstant:
var v ConstantCurve
if err := dec.Decode(&v); err != nil {
return fmt.Errorf("decode ConstantCurve: %w", err)
}
c.Constant = &v
case CurveParamFixed:
var v FixedCurve
if err := dec.Decode(&v); err != nil {
return fmt.Errorf("decode FixedCurve: %w", err)
}
c.Fixed = &v
case CurveParamLinear:
var v LinearCurve
if err := dec.Decode(&v); err != nil {
return fmt.Errorf("decode LinearCurve: %w", err)
}
c.Linear = &v
default:
return fmt.Errorf("unknown CurveParam tag: %d", tag)
}
return nil
}
type ConstantCurve struct {
TotalSupply uint64
TotalBaseSell uint64
TotalQuoteFundRaising uint64
MigrateType uint8
}
type FixedCurve struct {
TotalSupply uint64
TotalQuoteFundRaising uint64
MigrateType uint8
}
type LinearCurve struct {
TotalSupply uint64
TotalQuoteFundRaising uint64
MigrateType uint8
}
type BaseMintParam struct {
Decimals uint8
Name string
Symbol string
Uri string
}
type RaydiumLaunchLabCreateEvent struct {
Pool solana.PublicKey
Creator solana.PublicKey
Config solana.PublicKey
BaseMintParam BaseMintParam
CurveParam CurveParam
VestingParam VestingParam
ammFeeOn uint8 // 0 or 1, QuoteToken/BaseToken fee on amm swap
}
func raydiumLaunchLabInitializeParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
2026-02-11 14:58:12 +08:00
if len(instruction.Accounts) < 15 {
2026-02-09 14:46:19 +08:00
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for initialize instruction")
}
user := tx.rawTx.accountList[instruction.Accounts[0]]
creator := tx.rawTx.accountList[instruction.Accounts[1]]
pool := tx.rawTx.accountList[instruction.Accounts[5]]
platformConfig := tx.rawTx.accountList[instruction.Accounts[3]]
baseMint := tx.rawTx.accountList[instruction.Accounts[6]]
quoteMint := tx.rawTx.accountList[instruction.Accounts[7]]
baseVaultIdx := instruction.Accounts[8]
quoteVaultIdx := instruction.Accounts[9]
var (
baseTokenProgram solana.PublicKey
quoteTokenProgram solana.PublicKey
)
if bytes.Equal(instruction.Data[:8], raydiumLaunchLabInitializeWithToken2022PoolDiscriminator[:]) {
baseTokenProgram = tx.rawTx.accountList[instruction.Accounts[10]]
quoteTokenProgram = tx.rawTx.accountList[instruction.Accounts[11]]
} else if bytes.Equal(instruction.Data[:8], raydiumLaunchLabInitializeV2PoolDiscriminator[:]) {
baseTokenProgram = tx.rawTx.accountList[instruction.Accounts[11]]
quoteTokenProgram = tx.rawTx.accountList[instruction.Accounts[12]]
}
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultIdx)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get base vault balance after tx: %v", err)
}
quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultIdx)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get quote vault balance after tx: %v", err)
}
var programName string
if platformConfig.Equals(bonkPlatformConfig) {
programName = SolProgramRaydiumLaunchLabBonk
} else {
programName = SolProgramRaydiumLaunchLab
}
baseReserve, _ := decimal.NewFromString(baseTokenBalance.UITokenAmount.Amount)
quoteReserve, _ := decimal.NewFromString(quoteTokenBalance.UITokenAmount.Amount)
baseDecimals := uint8(baseTokenBalance.UITokenAmount.Decimals)
var createEvent RaydiumLaunchLabCreateEvent
inners, err := getInnerInstructions(innerInstructions, offset[1])
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get inner instructions: %v", err)
}
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
loadedEvent := false
var prefixLen uint = offset[1]
for innerIndex, innerInstruction := range inners {
if innerInstruction.ProgramIDIndex == instruction.ProgramIDIndex && len(innerInstruction.Data) >= 16 &&
bytes.Equal(innerInstruction.Data[:8], pumpEventDiscriminator[:]) &&
bytes.Equal(innerInstruction.Data[8:16], raydiumLaunchLabCreatePoolEvnet[:]) &&
len(innerInstruction.Accounts) == 1 {
if offset[1] == 0 {
offset[0] += 1
} else {
offset[1] = uint(innerIndex) + 1 + prefixLen
}
err := agbinary.NewBorshDecoder(innerInstruction.Data[16:]).Decode(&createEvent)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to deserialize create event: %w", err)
}
loadedEvent = true
break
}
}
if !loadedEvent {
return nil, increaseOffset(offset), fmt.Errorf("failed to get create event")
}
totalSupply := decimal.NewFromUint64(createEvent.CurveParam.TotalSupply()).Div(decimal.New(1, int32(baseDecimals)))
tx.Token[baseMint] = TokenMeta{
Mint: baseMint,
TokenProgram: baseTokenProgram,
Decimals: baseDecimals,
Name: createEvent.BaseMintParam.Name,
Symbol: createEvent.BaseMintParam.Symbol,
Url: createEvent.BaseMintParam.Uri,
TotalSupply: &totalSupply,
}
return []Swap{{
Program: programName,
Event: "create",
Pool: pool,
BaseMint: baseMint,
QuoteMint: quoteMint,
BaseTokenProgram: baseTokenProgram,
QuoteTokenProgram: quoteTokenProgram,
Creator: creator,
BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals),
QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals),
User: user,
BaseReserve: baseReserve,
QuoteReserve: quoteReserve,
EntryContract: entryContract,
}}, offset, nil
}
func raydiumLaunchLabMigrateToCpmmParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if len(instruction.Accounts) < 27 {
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for migrate instruction")
}
var entryContract solana.PublicKey = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
platformConfig := tx.rawTx.accountList[instruction.Accounts[3]]
var programName string
if platformConfig.Equals(bonkPlatformConfig) {
programName = SolProgramRaydiumLaunchLabBonk
} else {
programName = SolProgramRaydiumLaunchLab
}
baseTokenProgram := tx.rawTx.accountList[instruction.Accounts[22]]
quoteTokenProgram := tx.rawTx.accountList[instruction.Accounts[23]]
baseMint := tx.rawTx.accountList[instruction.Accounts[1]]
quoteMint := tx.rawTx.accountList[instruction.Accounts[2]]
pool := tx.rawTx.accountList[instruction.Accounts[17]]
baseVaultIdx := instruction.Accounts[19]
quoteVaultIdx := instruction.Accounts[20]
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultIdx)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get base vault balance after tx: %v", err)
}
quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultIdx)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get quote vault balance after tx: %v", err)
}
offset[1] += 1
return []Swap{
{
Program: programName,
Event: "migrate",
Pool: pool,
BaseMint: baseMint,
QuoteMint: quoteMint,
BaseTokenProgram: baseTokenProgram,
QuoteTokenProgram: quoteTokenProgram,
BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals),
QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals),
User: tx.rawTx.accountList[instruction.Accounts[0]],
//BaseAmount: decimal.Decimal{},
//QuoteAmount: decimal.Decimal{},
MigrateTopProgram: tx.rawTx.accountList[instruction.Accounts[4]],
MigrateToPool: tx.rawTx.accountList[instruction.Accounts[5]],
EntryContract: entryContract,
},
}, offset, nil
}
func raydiumLaunchLabMigrateToAmmParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if len(instruction.Accounts) < 27 {
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for migrate instruction")
}
platformConfig := tx.rawTx.accountList[instruction.Accounts[3]]
var programName string
if platformConfig.Equals(bonkPlatformConfig) {
programName = SolProgramRaydiumLaunchLabBonk
} else {
programName = SolProgramRaydiumLaunchLab
}
var entryContract solana.PublicKey = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
baseMint := tx.rawTx.accountList[instruction.Accounts[1]]
quoteMint := tx.rawTx.accountList[instruction.Accounts[2]]
pool := tx.rawTx.accountList[instruction.Accounts[23]]
baseVaultIdx := instruction.Accounts[25]
quoteVaultIdx := instruction.Accounts[26]
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultIdx)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get base vault balance after tx: %v", err)
}
quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultIdx)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get quote vault balance after tx: %v", err)
}
baseTokenProgram := baseTokenBalance.ProgramIDAccount
quoteTokenProgram := quoteTokenBalance.ProgramIDAccount
offset[1] += 1
return []Swap{
{
Program: programName,
Event: "migrate",
Pool: pool,
BaseMint: baseMint,
QuoteMint: quoteMint,
BaseTokenProgram: baseTokenProgram,
QuoteTokenProgram: quoteTokenProgram,
BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals),
QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals),
User: tx.rawTx.accountList[instruction.Accounts[0]],
//BaseAmount: decimal.Decimal{},
//QuoteAmount: decimal.Decimal{},
MigrateTopProgram: tx.rawTx.accountList[instruction.Accounts[12]],
MigrateToPool: tx.rawTx.accountList[instruction.Accounts[13]],
EntryContract: entryContract,
},
}, offset, nil
}
type RaydiumLaunchLabSwapEvent struct {
PoolState solana.PublicKey
TotalBaseSell uint64
VirtualBase uint64
VirtualQuote uint64
RealBaseBefore uint64
RealQuoteBefore uint64
RealBaseAfter uint64
RealQuoteAfter uint64
AmountIn uint64
AmountOut uint64
ProtocolFee uint64
PlatformFee uint64
CreatorFee uint64
ShareFee uint64
TradeDirection uint8 // 0: buy 1: sell
PoolStatus uint8 // 0 Fund, 1 Migrate, 2 Trade
}
2026-04-16 14:24:14 +08:00
type raydiumLaunchLabSwapArgs struct {
Amount uint64
OtherAmountThreshold uint64
}
2026-02-09 14:46:19 +08:00
func raydiumLaunchLabSwapParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
2026-05-28 10:23:56 +08:00
if len(instruction.Accounts) < 13 {
return nil, increaseOffset(offset), InstructionIgnoredError
}
2026-02-09 14:46:19 +08:00
platformConfig := tx.rawTx.accountList[instruction.Accounts[3]]
var programName string
if platformConfig.Equals(bonkPlatformConfig) {
programName = SolProgramRaydiumLaunchLabBonk
} else {
programName = SolProgramRaydiumLaunchLab
}
2026-04-16 14:24:14 +08:00
discriminator := *(*[8]byte)(instruction.Data[:8])
var swapMode SwapMode
var fixedAmount decimal.Decimal
var limitAmount decimal.Decimal
var swapArgs raydiumLaunchLabSwapArgs
if err := agbinary.NewBorshDecoder(instruction.Data[8:]).Decode(&swapArgs); err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to decode raydium launchlab swap args: %w", err)
}
switch discriminator {
case raydiumLaunchLabSellExactInDiscriminator, raydiumLaunchLabBuyExactInDiscriminator:
swapMode = SwapModeExactIn
fixedAmount = decimal.NewFromUint64(swapArgs.Amount)
limitAmount = decimal.NewFromUint64(swapArgs.OtherAmountThreshold)
case raydiumLaunchLabSellExactOutDiscriminator, raydiumLaunchLabBuyExactOutDiscriminator:
swapMode = SwapModeExactOut
fixedAmount = decimal.NewFromUint64(swapArgs.Amount)
limitAmount = decimal.NewFromUint64(swapArgs.OtherAmountThreshold)
default:
return nil, increaseOffset(offset), InstructionIgnoredError
}
2026-02-09 14:46:19 +08:00
var entryContract solana.PublicKey = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
user := tx.rawTx.accountList[instruction.Accounts[0]]
pool := tx.rawTx.accountList[instruction.Accounts[4]]
userBaseIdx := instruction.Accounts[5]
userQuoteIdx := instruction.Accounts[6]
baseVaultIdx := instruction.Accounts[7]
quoteVaultIdx := instruction.Accounts[8]
baseMint := tx.rawTx.accountList[instruction.Accounts[9]]
quoteMint := tx.rawTx.accountList[instruction.Accounts[10]]
baseTokenProgram := tx.rawTx.accountList[instruction.Accounts[11]]
quoteTokenProgram := tx.rawTx.accountList[instruction.Accounts[12]]
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultIdx)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get base vault balance after tx: %v", err)
}
quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultIdx)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get quote vault balance after tx: %v", err)
}
baseMintDecimals := uint8(baseTokenBalance.UITokenAmount.Decimals)
quoteMintDecimals := uint8(quoteTokenBalance.UITokenAmount.Decimals)
inners, err := getInnerInstructions(innerInstructions, offset[1])
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get inner instructions: %v", err)
}
var swapEvent RaydiumLaunchLabSwapEvent
loadedEvent := false
var prefixLen uint = offset[1]
for innerIndex, innerInstruction := range inners {
if innerInstruction.ProgramIDIndex == instruction.ProgramIDIndex && len(innerInstruction.Data) >= 16 &&
bytes.Equal(innerInstruction.Data[:8], pumpEventDiscriminator[:]) &&
bytes.Equal(innerInstruction.Data[8:16], raydiumLaunchLabTradeEvnet[:]) &&
len(innerInstruction.Accounts) == 1 {
if offset[1] == 0 {
offset[0] += 1
} else {
offset[1] = uint(innerIndex) + 1 + prefixLen
}
err := agbinary.NewBorshDecoder(innerInstruction.Data[16:]).Decode(&swapEvent)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to deserialize swap event: %w", err)
}
loadedEvent = true
break
}
}
if !loadedEvent {
return nil, increaseOffset(offset), fmt.Errorf("failed to get swap event")
}
var event string
var baseAmount, quoteAmount decimal.Decimal
if swapEvent.TradeDirection == 0 {
event = "buy"
baseAmount = decimal.NewFromInt(int64(swapEvent.AmountOut))
quoteAmount = decimal.NewFromInt(int64(swapEvent.AmountIn))
} else {
event = "sell"
baseAmount = decimal.NewFromInt(int64(swapEvent.AmountIn))
quoteAmount = decimal.NewFromInt(int64(swapEvent.AmountOut))
}
baseReserve := decimal.NewFromInt(int64(swapEvent.RealBaseAfter))
quoteReserve := decimal.NewFromInt(int64(swapEvent.RealQuoteAfter))
userBase := getAccountBalanceAfterTx(tx.rawTx, userBaseIdx)
userQuote := getAccountBalanceAfterTx(tx.rawTx, userQuoteIdx)
2026-04-16 14:24:14 +08:00
swap := Swap{
2026-02-09 14:46:19 +08:00
Program: programName,
Event: event,
Pool: pool,
BaseMint: baseMint,
QuoteMint: quoteMint,
BaseTokenProgram: baseTokenProgram,
QuoteTokenProgram: quoteTokenProgram,
BaseMintDecimals: baseMintDecimals,
QuoteMintDecimals: quoteMintDecimals,
User: user,
BaseAmount: baseAmount,
QuoteAmount: quoteAmount,
BaseReserve: baseReserve,
QuoteReserve: quoteReserve,
Mayhem: false,
UserBaseBalance: userBase,
UserQuoteBalance: userQuote,
EntryContract: entryContract,
2026-04-16 14:24:14 +08:00
}
swap.SetSwapAmountInfo(swapMode, fixedAmount, limitAmount)
return []Swap{swap}, offset, nil
2026-02-09 14:46:19 +08:00
}