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) { if len(instruction.Accounts) < 15 { 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 } type raydiumLaunchLabSwapArgs struct { Amount uint64 OtherAmountThreshold uint64 } func raydiumLaunchLabSwapParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { platformConfig := tx.rawTx.accountList[instruction.Accounts[3]] var programName string if platformConfig.Equals(bonkPlatformConfig) { programName = SolProgramRaydiumLaunchLabBonk } else { programName = SolProgramRaydiumLaunchLab } 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 } 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) swap := Swap{ 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, } swap.SetSwapAmountInfo(swapMode, fixedAmount, limitAmount) return []Swap{swap}, offset, nil }