package pump_parser import ( "bytes" "fmt" agbinary "github.com/gagliardetto/binary" "github.com/gagliardetto/solana-go" "github.com/shopspring/decimal" ) func increaseOffset(offset [2]uint) [2]uint { if offset[1] == 0 { return [2]uint{offset[0] + 1, offset[1]} } return [2]uint{offset[0], offset[1] + 1} } // pumpParser // routes pump program instructions to their respective parsers, // offset is [outerIndex, innerIndex] index of instructions in the transaction, // if innerIndex == 0 this is outer instruction,if it's an inner instruction, outerIndex is the index of the parent instruction. func pumpParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { if !tx.rawTx.accountList[instruction.ProgramIDIndex].Equals(pumpProgram) { return nil, increaseOffset(offset), fmt.Errorf("pump program instruction not found, offset, %d, %d", offset[0], offset[1]) } decode := instruction.Data if len(decode) < 8 { return nil, increaseOffset(offset), fmt.Errorf("pump program instruction data too short, offset, %d, %d", offset[0], offset[1]) } discriminator := *(*[8]byte)(decode[:8]) switch discriminator { case pumpBuyExactSolInDiscriminator, pumpBuyDiscriminator, pumpBuyV2Discriminator, pumpBuyExactQuoteInV2Discriminator, pumpSellDiscriminator, pumpSellV2Discriminator: if tx.Err != nil { return failedTxBuyOrSellParser(tx, instruction, innerInstructions, offset) } return BuyOrSellParser(tx, instruction, innerInstructions, offset) case pumpCreateDiscriminator, pumpCreateV2Discriminator: if tx.Err != nil { return nil, increaseOffset(offset), InstructionIgnoredError } return CreateParser(tx, instruction, innerInstructions, offset) case pumpMigrateDiscriminator, pumpMigrateV2Discriminator: if tx.Err != nil { return nil, increaseOffset(offset), InstructionIgnoredError } return MigrateParser(tx, instruction, innerInstructions, offset) default: return nil, increaseOffset(offset), InstructionIgnoredError } } type PumpCreateData struct { Discriminator uint64 Name string Symbol string Uri string Creator solana.PublicKey } type PumpCreateV2Data struct { Discriminator uint64 Name string Symbol string Uri string Creator solana.PublicKey IsMayhem bool } type PumpCreateEvent struct { Name string Symbol string Uri string Mint solana.PublicKey BondingCurve solana.PublicKey User solana.PublicKey Creator solana.PublicKey Timestamp int64 VirtualTokenReserves uint64 VirtualSolReserves uint64 RealTokenReserves uint64 TokenTotalSupply uint64 TokenProgram solana.PublicKey IsMayhemMode bool IsCashbackEnabled bool QuoteMint solana.PublicKey VirtualQuoteReserves uint64 } type pumpCreateEventLegacy struct { Name string Symbol string Uri string Mint solana.PublicKey BondingCurve solana.PublicKey User solana.PublicKey Creator solana.PublicKey Timestamp int64 VirtualTokenReserves uint64 VirtualSolReserves uint64 RealTokenReserves uint64 TokenTotalSupply uint64 TokenProgram solana.PublicKey IsMayhemMode bool IsCashbackEnabled bool } func decodePumpCreateEvent(data []byte) (PumpCreateEvent, error) { var event PumpCreateEvent if err := agbinary.NewBorshDecoder(data).Decode(&event); err == nil { return event, nil } var legacy pumpCreateEventLegacy if err := agbinary.NewBorshDecoder(data).Decode(&legacy); err != nil { return PumpCreateEvent{}, err } return PumpCreateEvent{ Name: legacy.Name, Symbol: legacy.Symbol, Uri: legacy.Uri, Mint: legacy.Mint, BondingCurve: legacy.BondingCurve, User: legacy.User, Creator: legacy.Creator, Timestamp: legacy.Timestamp, VirtualTokenReserves: legacy.VirtualTokenReserves, VirtualSolReserves: legacy.VirtualSolReserves, RealTokenReserves: legacy.RealTokenReserves, TokenTotalSupply: legacy.TokenTotalSupply, TokenProgram: legacy.TokenProgram, IsMayhemMode: legacy.IsMayhemMode, IsCashbackEnabled: legacy.IsCashbackEnabled, }, nil } func CreateParser(tx *Tx, instr Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { result := tx.rawTx var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] var programIndex = instr.ProgramIDIndex var err error var createEvent PumpCreateEvent var prefixLen = offset[1] inners, err := getInnerInstructions(innerInstructions, prefixLen) if err != nil { return nil, increaseOffset(offset), fmt.Errorf("pump create get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1]) } for innerIndex, innerInstr := range inners { if innerInstr.ProgramIDIndex == programIndex && bytes.Equal(innerInstr.Data[:8], pumpEventDiscriminator[:]) && bytes.Equal(innerInstr.Data[8:16], pumpCreateEventDiscriminator[:]) { createEvent, err = decodePumpCreateEvent(innerInstr.Data[16:]) if offset[1] == 0 { offset[0] += 1 } else { offset[1] = uint(innerIndex) + 1 + prefixLen } if err != nil { return nil, offset, fmt.Errorf("pump create event decode error: %v, offset, %d, %d", err, offset[0], offset[1]) } break } } if createEvent == (PumpCreateEvent{}) { return nil, increaseOffset(offset), fmt.Errorf("pump create event not found, offset, %d, %d", offset[0], offset[1]) } userIndex := 0 if bytes.HasPrefix(instr.Data, pumpCreateV2Discriminator[:]) { userIndex = instr.Accounts[5] } else if bytes.HasPrefix(instr.Data, pumpCreateDiscriminator[:]) { userIndex = instr.Accounts[7] } userBase := getAccountBalanceAfterTx(result, userIndex) userQuote, _ := GetSolAfterTx(result, userIndex) quoteMint, quoteTokenProgram, quoteDecimals := pumpCreateQuoteAccounts(result, instr, createEvent) userQuoteBalance := decimal.NewFromUint64(userQuote) if !quoteMint.IsZero() && !quoteMint.Equals(wSolMint) { userQuoteBalance = GetTokenBalanceAfterTx(result, userIndex, quoteTokenProgram, quoteMint) } totalSupply := decimal.NewFromUint64(createEvent.TokenTotalSupply).Div(decimal.New(1, 6)) tx.Token[createEvent.Mint] = TokenMeta{ Mint: createEvent.Mint, TokenProgram: createEvent.TokenProgram, Decimals: 6, Name: createEvent.Name, Symbol: createEvent.Symbol, Url: createEvent.Uri, TotalSupply: &totalSupply, } return []Swap{ { Program: SolProgramPump, Event: "create", Pool: createEvent.BondingCurve, BaseMint: createEvent.Mint, QuoteMint: quoteMint, BaseTokenProgram: createEvent.TokenProgram, QuoteTokenProgram: quoteTokenProgram, Creator: createEvent.Creator, BaseMintDecimals: 6, QuoteMintDecimals: quoteDecimals, User: createEvent.User, BaseAmount: decimal.Zero, QuoteAmount: decimal.Zero, BaseReserve: decimal.NewFromUint64(createEvent.RealTokenReserves), QuoteReserve: decimal.Zero, Mayhem: createEvent.IsMayhemMode, Cashback: createEvent.IsCashbackEnabled, UserBaseBalance: userBase, UserQuoteBalance: userQuoteBalance, EntryContract: entryContract, }, }, offset, nil } type PumpTradeEvent struct { Mint solana.PublicKey SolAmount uint64 TokenAmount uint64 IsBuy bool User solana.PublicKey Timestamp int64 VirtualSolReserves uint64 VirtualTokenReserves uint64 RealSolReserves uint64 RealTokenReserves uint64 FeeRecipient solana.PublicKey FeeBasisPoints uint64 Fee uint64 Creator solana.PublicKey CreatorFeeBasisPoints uint64 CreatorFee uint64 TrackVolume bool TotalUnclaimedTokens uint64 TotalClaimedTokens uint64 CurrentSolVolume uint64 LastUpdateTimestamp int64 IxName string MayhemMode bool CashbackFeeBasisPoints uint64 Cashback uint64 BuybackFeeBasisPoints uint64 BuybackFee uint64 Shareholders []PumpShareholder QuoteMint solana.PublicKey QuoteAmount uint64 VirtualQuoteReserves uint64 RealQuoteReserves uint64 } type PumpShareholder struct { Address solana.PublicKey ShareBps uint16 } type pumpTradeEventLegacy struct { Mint solana.PublicKey SolAmount uint64 TokenAmount uint64 IsBuy bool User solana.PublicKey Timestamp int64 VirtualSolReserves uint64 VirtualTokenReserves uint64 RealSolReserves uint64 RealTokenReserves uint64 FeeRecipient solana.PublicKey FeeBasisPoints uint64 Fee uint64 Creator solana.PublicKey CreatorFeeBasisPoints uint64 CreatorFee uint64 TrackVolume bool TotalUnclaimedTokens uint64 TotalClaimedTokens uint64 CurrentSolVolume uint64 LastUpdateTimestamp int64 IxName string MayhemMode bool CashbackFeeBasisPoints uint64 Cashback uint64 } type pumpTradeEventLegacyV0 struct { Mint solana.PublicKey SolAmount uint64 TokenAmount uint64 IsBuy bool User solana.PublicKey Timestamp int64 VirtualSolReserves uint64 VirtualTokenReserves uint64 RealSolReserves uint64 RealTokenReserves uint64 FeeRecipient solana.PublicKey FeeBasisPoints uint64 Fee uint64 Creator solana.PublicKey CreatorFeeBasisPoints uint64 CreatorFee uint64 TrackVolume bool TotalUnclaimedTokens uint64 TotalClaimedTokens uint64 CurrentSolVolume uint64 LastUpdateTimestamp int64 IxName string } func decodePumpTradeEvent(data []byte) (PumpTradeEvent, error) { var event PumpTradeEvent if err := agbinary.NewBorshDecoder(data).Decode(&event); err == nil { return event, nil } var legacy pumpTradeEventLegacy if err := agbinary.NewBorshDecoder(data).Decode(&legacy); err == nil { return PumpTradeEvent{ Mint: legacy.Mint, SolAmount: legacy.SolAmount, TokenAmount: legacy.TokenAmount, IsBuy: legacy.IsBuy, User: legacy.User, Timestamp: legacy.Timestamp, VirtualSolReserves: legacy.VirtualSolReserves, VirtualTokenReserves: legacy.VirtualTokenReserves, RealSolReserves: legacy.RealSolReserves, RealTokenReserves: legacy.RealTokenReserves, FeeRecipient: legacy.FeeRecipient, FeeBasisPoints: legacy.FeeBasisPoints, Fee: legacy.Fee, Creator: legacy.Creator, CreatorFeeBasisPoints: legacy.CreatorFeeBasisPoints, CreatorFee: legacy.CreatorFee, TrackVolume: legacy.TrackVolume, TotalUnclaimedTokens: legacy.TotalUnclaimedTokens, TotalClaimedTokens: legacy.TotalClaimedTokens, CurrentSolVolume: legacy.CurrentSolVolume, LastUpdateTimestamp: legacy.LastUpdateTimestamp, IxName: legacy.IxName, MayhemMode: legacy.MayhemMode, CashbackFeeBasisPoints: legacy.CashbackFeeBasisPoints, Cashback: legacy.Cashback, }, nil } var legacyV0 pumpTradeEventLegacyV0 if err := agbinary.NewBorshDecoder(data).Decode(&legacyV0); err != nil { return PumpTradeEvent{}, err } return PumpTradeEvent{ Mint: legacyV0.Mint, SolAmount: legacyV0.SolAmount, TokenAmount: legacyV0.TokenAmount, IsBuy: legacyV0.IsBuy, User: legacyV0.User, Timestamp: legacyV0.Timestamp, VirtualSolReserves: legacyV0.VirtualSolReserves, VirtualTokenReserves: legacyV0.VirtualTokenReserves, RealSolReserves: legacyV0.RealSolReserves, RealTokenReserves: legacyV0.RealTokenReserves, FeeRecipient: legacyV0.FeeRecipient, FeeBasisPoints: legacyV0.FeeBasisPoints, Fee: legacyV0.Fee, Creator: legacyV0.Creator, CreatorFeeBasisPoints: legacyV0.CreatorFeeBasisPoints, CreatorFee: legacyV0.CreatorFee, TrackVolume: legacyV0.TrackVolume, TotalUnclaimedTokens: legacyV0.TotalUnclaimedTokens, TotalClaimedTokens: legacyV0.TotalClaimedTokens, CurrentSolVolume: legacyV0.CurrentSolVolume, LastUpdateTimestamp: legacyV0.LastUpdateTimestamp, IxName: legacyV0.IxName, }, nil } type PumpTradeFeeArg struct { IsPump bool MarketCap [16]byte TradeSize uint64 } type CompleteEvent struct { User solana.PublicKey Mint solana.PublicKey BondingCurve solana.PublicKey Timestamp int64 } type PumpTradeArgs struct { Discriminator [8]byte Amount1 uint64 Amount2 uint64 } func pumpTradeAmountInfoFromArgs(args PumpTradeArgs) (swapMode SwapMode, fixedAmount decimal.Decimal, limitAmount decimal.Decimal, ok bool) { switch { case bytes.Equal(args.Discriminator[:], pumpBuyExactSolInDiscriminator[:]), bytes.Equal(args.Discriminator[:], pumpBuyExactQuoteInV2Discriminator[:]): return SwapModeExactIn, decimal.NewFromUint64(args.Amount1), decimal.NewFromUint64(args.Amount2), true case bytes.Equal(args.Discriminator[:], pumpBuyDiscriminator[:]), bytes.Equal(args.Discriminator[:], pumpBuyV2Discriminator[:]): return SwapModeExactOut, decimal.NewFromUint64(args.Amount1), decimal.NewFromUint64(args.Amount2), true case bytes.Equal(args.Discriminator[:], pumpSellDiscriminator[:]), bytes.Equal(args.Discriminator[:], pumpSellV2Discriminator[:]): return SwapModeExactIn, decimal.NewFromUint64(args.Amount1), decimal.NewFromUint64(args.Amount2), true default: return SwapModeUnknown, decimal.Zero, decimal.Zero, false } } type pumpTradeAccountLayout struct { IsV2 bool FeeRecipient int BaseMint int QuoteMint int BaseTokenProgram int QuoteTokenProgram int Pool int BasePoolToken int QuotePoolToken int User int BaseUserToken int QuoteUserToken int } func pumpTradeLayout(instr Instruction) (pumpTradeAccountLayout, bool) { if len(instr.Data) < 8 { return pumpTradeAccountLayout{}, false } discriminator := instr.Data[:8] switch { case bytes.Equal(discriminator, pumpBuyDiscriminator[:]), bytes.Equal(discriminator, pumpBuyExactSolInDiscriminator[:]): if len(instr.Accounts) <= 8 { return pumpTradeAccountLayout{}, false } return pumpTradeAccountLayout{ FeeRecipient: 1, BaseMint: 2, QuoteMint: -1, BaseTokenProgram: 8, QuoteTokenProgram: -1, Pool: 3, BasePoolToken: 4, QuotePoolToken: -1, User: 6, BaseUserToken: 5, QuoteUserToken: -1, }, true case bytes.Equal(discriminator, pumpSellDiscriminator[:]): if len(instr.Accounts) <= 9 { return pumpTradeAccountLayout{}, false } return pumpTradeAccountLayout{ FeeRecipient: 1, BaseMint: 2, QuoteMint: -1, BaseTokenProgram: 9, QuoteTokenProgram: -1, Pool: 3, BasePoolToken: 4, QuotePoolToken: -1, User: 6, BaseUserToken: 5, QuoteUserToken: -1, }, true case bytes.Equal(discriminator, pumpBuyV2Discriminator[:]), bytes.Equal(discriminator, pumpBuyExactQuoteInV2Discriminator[:]), bytes.Equal(discriminator, pumpSellV2Discriminator[:]): if len(instr.Accounts) <= 15 { return pumpTradeAccountLayout{}, false } return pumpTradeAccountLayout{ IsV2: true, FeeRecipient: 6, BaseMint: 1, QuoteMint: 2, BaseTokenProgram: 3, QuoteTokenProgram: 4, Pool: 10, BasePoolToken: 11, QuotePoolToken: 12, User: 13, BaseUserToken: 14, QuoteUserToken: 15, }, true default: return pumpTradeAccountLayout{}, false } } func pumpInstructionIsSell(data []byte) bool { return len(data) >= 8 && (bytes.Equal(data[:8], pumpSellDiscriminator[:]) || bytes.Equal(data[:8], pumpSellV2Discriminator[:])) } func pumpInstructionIsExactQuoteIn(data []byte) bool { return len(data) >= 8 && (bytes.Equal(data[:8], pumpBuyExactSolInDiscriminator[:]) || bytes.Equal(data[:8], pumpBuyExactQuoteInV2Discriminator[:])) } func pumpAccount(result *RawTx, instr Instruction, accountIndex int) solana.PublicKey { if accountIndex < 0 || accountIndex >= len(instr.Accounts) { return solana.PublicKey{} } listIndex := instr.Accounts[accountIndex] if listIndex < 0 || listIndex >= len(result.accountList) { return solana.PublicKey{} } return result.accountList[listIndex] } func pumpCreateQuoteAccounts(result *RawTx, instr Instruction, createEvent PumpCreateEvent) (solana.PublicKey, solana.PublicKey, uint8) { quoteMint := createEvent.QuoteMint quoteTokenProgram := solana.PublicKey{} optionalStart := -1 if len(instr.Data) >= 8 && bytes.Equal(instr.Data[:8], pumpCreateV2Discriminator[:]) { optionalStart = 16 } if optionalStart >= 0 && len(instr.Accounts) > optionalStart { accountQuoteMint := pumpAccount(result, instr, optionalStart) if quoteMint.IsZero() && !accountQuoteMint.IsZero() && !accountQuoteMint.Equals(wSolMint) { quoteMint = accountQuoteMint } if len(instr.Accounts) > optionalStart+2 && !quoteMint.IsZero() { quoteTokenProgram = pumpAccount(result, instr, optionalStart+2) } } if quoteMint.Equals(wSolMint) { quoteTokenProgram = solana.TokenProgramID } if quoteTokenProgram.IsZero() && !quoteMint.IsZero() { quoteTokenProgram = solana.TokenProgramID } return quoteMint, quoteTokenProgram, pumpQuoteDecimals(result, quoteMint) } func pumpMintDecimalsFromBalances(result *RawTx, mint solana.PublicKey, fallback uint8) uint8 { if mint.IsZero() { return fallback } for _, balance := range result.Meta.PostTokenBalances { balance.ParseAccount() if balance.MintAccount.Equals(mint) { return uint8(balance.UITokenAmount.Decimals) } } for _, balance := range result.Meta.PreTokenBalances { balance.ParseAccount() if balance.MintAccount.Equals(mint) { return uint8(balance.UITokenAmount.Decimals) } } return fallback } func pumpQuoteDecimals(result *RawTx, quoteMint solana.PublicKey) uint8 { fallback := uint8(9) if quoteMint.Equals(usdcMint) || quoteMint.Equals(usd1Mint) { fallback = 6 } return pumpMintDecimalsFromBalances(result, quoteMint, fallback) } func pumpQuoteAmount(tradeEvent PumpTradeEvent) uint64 { if tradeEvent.QuoteAmount != 0 { return tradeEvent.QuoteAmount } return tradeEvent.SolAmount } func pumpQuoteReserve(tradeEvent PumpTradeEvent) uint64 { if tradeEvent.RealQuoteReserves != 0 { return tradeEvent.RealQuoteReserves } return tradeEvent.RealSolReserves } func pumpCompleteMatchesTradeEvent(completeEvent CompleteEvent, tradeEvent PumpTradeEvent, bondingCurve solana.PublicKey) bool { if completeEvent.Mint != tradeEvent.Mint { return false } if completeEvent.User != tradeEvent.User { return false } if completeEvent.BondingCurve != bondingCurve { return false } return true } func normalizePumpQuoteSideMint(s *Swap) { if s.FixedAmountSide == SwapAmountSideQuote && s.FixedMint.IsZero() { s.FixedMint = wSolMint } if s.LimitAmountSide == SwapAmountSideQuote && s.LimitMint.IsZero() { s.LimitMint = wSolMint } if s.ActualLimitAmountSide == SwapAmountSideQuote && s.LimitMint.IsZero() { s.LimitMint = wSolMint } } func failedTxBuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { if tx.Err == nil || tx.Err.UnKnown != "" { return nil, increaseOffset(offset), fmt.Errorf("tx pump failed but error is nil, offset, %d, %d", offset[0], offset[1]) } if tx.Err.Variant != InstructionError { return nil, increaseOffset(offset), fmt.Errorf("failed tx pump failed but error variant is not instruction error, offset, %d, %d", offset[0], offset[1]) } if tx.Err.Enum != Custom && tx.Err.Enum != ComputationalBudgetExceeded && tx.Err.Enum != ProgramFailedToComplete { return nil, increaseOffset(offset), fmt.Errorf("failed tx pump failed but error is not custom or computational budget exceeded, offset, %d, %d", offset[0], offset[1]) } if tx.Err.Enum == Custom { if !(tx.Err.CustomCode == 1 || tx.Err.CustomCode == 6042 || tx.Err.CustomCode == 6041 || tx.Err.CustomCode == 6040 || tx.Err.CustomCode == 6023 || tx.Err.CustomCode == 6021 || tx.Err.CustomCode == 6003 || tx.Err.CustomCode == 6002) { return nil, increaseOffset(offset), fmt.Errorf("failed tx pump failed but custom error code is unexpected, offset, %d, %d, code: %d", offset[0], offset[1], tx.Err.CustomCode) } } result := tx.rawTx var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] layout, ok := pumpTradeLayout(instruction) if !ok { return nil, increaseOffset(offset), fmt.Errorf("unknown pump trade instruction account layout, offset, %d, %d", offset[0], offset[1]) } user := pumpAccount(result, instruction, layout.User) ataUserIdx := instruction.Accounts[layout.BaseUserToken] userIndex := instruction.Accounts[layout.User] mint := pumpAccount(result, instruction, layout.BaseMint) quoteMint := pumpAccount(result, instruction, layout.QuoteMint) quoteTokenProgram := pumpAccount(result, instruction, layout.QuoteTokenProgram) var args PumpTradeArgs err := agbinary.NewBorshDecoder(instruction.Data[:]).Decode(&args) if err != nil { return nil, increaseOffset(offset), fmt.Errorf("failed tx pump buy/sell decode error: %v, offset, %d, %d", err, offset[0], offset[1]) } var event string var ( quoteAmount, tokenAmount uint64 ) if bytes.Equal(args.Discriminator[:], pumpBuyExactSolInDiscriminator[:]) || bytes.Equal(args.Discriminator[:], pumpBuyExactQuoteInV2Discriminator[:]) { event = "buy_failed" quoteAmount = args.Amount1 tokenAmount = args.Amount2 } else if bytes.Equal(args.Discriminator[:], pumpBuyDiscriminator[:]) || bytes.Equal(args.Discriminator[:], pumpBuyV2Discriminator[:]) { event = "buy_failed" quoteAmount = args.Amount2 tokenAmount = args.Amount1 } else if bytes.Equal(args.Discriminator[:], pumpSellDiscriminator[:]) || bytes.Equal(args.Discriminator[:], pumpSellV2Discriminator[:]) { event = "sell_failed" quoteAmount = args.Amount2 tokenAmount = args.Amount1 } else { return nil, increaseOffset(offset), fmt.Errorf("unknown pump trade instruction discriminator, offset, %d, %d", offset[0], offset[1]) } baseTokenProgram := pumpAccount(result, instruction, layout.BaseTokenProgram) if !user.IsOnCurve() && (entryContract.Equals(okxDexRoutersV2) || entryContract.Equals(okxAggregatorV2)) { userBaseAmount, ataIndex := tokenBalanceChange(result, 0, baseTokenProgram, mint) //&& userBaseAmount.BigInt().Uint64() == tradeEvent.TokenAmount if !userBaseAmount.IsZero() { user = result.accountList[0] userIndex = 0 ataUserIdx = ataIndex } } userBase := getAccountBalanceAfterTx(result, ataUserIdx) userQuote := decimal.Zero if layout.IsV2 && !quoteMint.Equals(wSolMint) { userQuote = getAccountBalanceAfterTx(result, instruction.Accounts[layout.QuoteUserToken]) } else { userQuoteLamports, _ := GetSolAfterTx(result, userIndex) userQuote = decimal.NewFromUint64(userQuoteLamports) } bcIdx := instruction.Accounts[layout.Pool] bcAtaIndex := instruction.Accounts[layout.BasePoolToken] quoteReserves := decimal.Zero if layout.IsV2 && !quoteMint.Equals(wSolMint) { quoteReserves = getAccountBalanceAfterTx(result, instruction.Accounts[layout.QuotePoolToken]) } else { solReserves, _ := GetSolAfterTx(result, bcIdx) quoteReserves = decimal.NewFromUint64(solReserves) } tokenReserves := getAccountBalanceAfterTx(result, bcAtaIndex) swaps := []Swap{ { Program: SolProgramPump, Event: event, Pool: pumpAccount(result, instruction, layout.Pool), BaseMint: mint, QuoteMint: quoteMint, BaseTokenProgram: baseTokenProgram, QuoteTokenProgram: quoteTokenProgram, BaseMintDecimals: 6, QuoteMintDecimals: pumpQuoteDecimals(result, quoteMint), User: user, BaseAmount: decimal.NewFromUint64(tokenAmount), QuoteAmount: decimal.NewFromUint64(quoteAmount), BaseReserve: tokenReserves, QuoteReserve: quoteReserves, Mayhem: isMayhemPump(pumpAccount(result, instruction, layout.FeeRecipient)), UserBaseBalance: userBase, UserQuoteBalance: userQuote, EntryContract: entryContract, }, } if swapMode, fixedAmount, limitAmount, ok := pumpTradeAmountInfoFromArgs(args); ok { swaps[0].SetSwapAmountInfo(swapMode, fixedAmount, limitAmount) normalizePumpQuoteSideMint(&swaps[0]) } return swaps, offset, nil } func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { result := tx.rawTx var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] var err error var programIndex = instruction.ProgramIDIndex layout, ok := pumpTradeLayout(instruction) if !ok { return nil, increaseOffset(offset), fmt.Errorf("unknown pump trade instruction account layout, offset, %d, %d", offset[0], offset[1]) } feeEventProgramIndex := 0 for i, b := range result.accountList { if b.Equals(pumpFeesProgram) { feeEventProgramIndex = i break } } var ( tradeEvent PumpTradeEvent tradeFeeArg PumpTradeFeeArg completeEvent CompleteEvent completed bool newoffset [2]uint tradeFound bool ) var prefixLen = offset[1] inners, err := getInnerInstructions(innerInstructions, prefixLen) if err != nil { return nil, increaseOffset(offset), fmt.Errorf("pump create get inner instructions error: %v,offset, %d, %d", err, offset[0], offset[1]) } if !entryContract.Equals(axiomOuterContract) { if instruction.StackHeight != nil && *instruction.StackHeight > 2 { for _, innerInstr := range innerInstructions.Instructions { if innerInstr.StackHeight != nil && *innerInstr.StackHeight == *instruction.StackHeight-1 { entryContract = result.accountList[innerInstr.ProgramIDIndex] break } } } } for innerIndex, innerInstr := range inners { if innerInstr.ProgramIDIndex == feeEventProgramIndex && bytes.Equal(innerInstr.Data[:8], pumpGetFeesDiscriminator[:]) { err = agbinary.NewBorshDecoder(innerInstr.Data[8:]).Decode(&tradeFeeArg) if err != nil { return nil, increaseOffset(offset), fmt.Errorf("pump get fees event decode error: %v, offset, %d, %d", err, offset[0], offset[1]) } continue } if innerInstr.ProgramIDIndex == programIndex && bytes.Equal(innerInstr.Data[:8], pumpEventDiscriminator[:]) { if bytes.Equal(innerInstr.Data[8:16], pumpTradeEventDiscriminator[8:16]) { if tradeFound { break } tradeEvent, err = decodePumpTradeEvent(innerInstr.Data[16:]) if offset[1] == 0 { newoffset = [2]uint{offset[0] + 1, offset[1]} } else { newoffset = [2]uint{offset[0], prefixLen + uint(innerIndex) + 1} } if err != nil { return nil, newoffset, fmt.Errorf("pump buy event decode error: %v, offset, %d, %d", err, offset[0], offset[1]) } expectedIsBuy := !pumpInstructionIsSell(instruction.Data) if tradeEvent.IsBuy != expectedIsBuy { tradeEvent = PumpTradeEvent{} continue } tradeFound = true if !tradeEvent.IsBuy { break } } else if bytes.Equal(innerInstr.Data[8:16], pumpCompleteEventDiscriminator[:]) { if !tradeFound { continue } err = agbinary.NewBorshDecoder(innerInstr.Data[16:]).Decode(&completeEvent) if err != nil { return nil, increaseOffset(offset), fmt.Errorf("pump completeEvent event decode error: %v, offset, %d, %d", err, offset[0], offset[1]) } if !pumpCompleteMatchesTradeEvent(completeEvent, tradeEvent, pumpAccount(result, instruction, layout.Pool)) { break } if offset[1] == 0 { newoffset = [2]uint{offset[0] + 1, offset[1]} } else { newoffset = [2]uint{offset[0], prefixLen + uint(innerIndex) + 1} } completed = true break } } } if !tradeFound { return nil, increaseOffset(offset), fmt.Errorf("pmp buy/sell event not found, offset, %d, %d", offset[0], offset[1]) } offset = [2]uint{newoffset[0], newoffset[1]} var args PumpTradeArgs if err := agbinary.NewBorshDecoder(instruction.Data[:]).Decode(&args); err != nil { return nil, increaseOffset(offset), fmt.Errorf("failed tx pump buy/sell decode error: %v, offset, %d, %d", err, offset[0], offset[1]) } event := "" baseTokenProgram := pumpAccount(result, instruction, layout.BaseTokenProgram) quoteMint := tradeEvent.QuoteMint if quoteMint.IsZero() { quoteMint = pumpAccount(result, instruction, layout.QuoteMint) } quoteTokenProgram := pumpAccount(result, instruction, layout.QuoteTokenProgram) if tradeEvent.IsBuy { event = "buy" } else { event = "sell" } if _, exists := tx.Token[tradeEvent.Mint]; !exists { tx.Token[tradeEvent.Mint] = TokenMeta{ Mint: tradeEvent.Mint, TokenProgram: baseTokenProgram, Decimals: 6, } } var user = tradeEvent.User ataUserIdx := instruction.Accounts[layout.BaseUserToken] userIndex := instruction.Accounts[layout.User] if !tradeEvent.User.IsOnCurve() && (entryContract.Equals(okxDexRoutersV2) || entryContract.Equals(okxAggregatorV2)) { userBaseAmount, ataIndex := tokenBalanceChange(result, 0, baseTokenProgram, tradeEvent.Mint) //&& userBaseAmount.BigInt().Uint64() == tradeEvent.TokenAmount if !userBaseAmount.IsZero() { user = result.accountList[0] userIndex = 0 ataUserIdx = ataIndex } } userBase := getAccountBalanceAfterTx(result, ataUserIdx) userQuote := decimal.Zero if layout.IsV2 && !quoteMint.Equals(wSolMint) { userQuote = getAccountBalanceAfterTx(result, instruction.Accounts[layout.QuoteUserToken]) } else { userQuoteLamports, _ := GetSolAfterTx(result, userIndex) userQuote = decimal.NewFromUint64(userQuoteLamports) } quoteAmount := pumpQuoteAmount(tradeEvent) if tradeEvent.IsBuy && pumpInstructionIsExactQuoteIn(instruction.Data) && !layout.IsV2 { fee := tradeEvent.Fee + tradeEvent.CreatorFee quoteAmount = tradeFeeArg.TradeSize if quoteAmount > fee { quoteAmount = quoteAmount - fee } } isCashbackCoin := tradeEvent.CashbackFeeBasisPoints > 0 || tradeEvent.Cashback > 0 swaps := []Swap{ { Program: SolProgramPump, Event: event, Pool: pumpAccount(result, instruction, layout.Pool), BaseMint: tradeEvent.Mint, QuoteMint: quoteMint, BaseTokenProgram: baseTokenProgram, QuoteTokenProgram: quoteTokenProgram, Creator: tradeEvent.Creator, BaseMintDecimals: 6, QuoteMintDecimals: pumpQuoteDecimals(result, quoteMint), User: user, BaseAmount: decimal.NewFromUint64(tradeEvent.TokenAmount), QuoteAmount: decimal.NewFromUint64(quoteAmount), BaseReserve: decimal.NewFromUint64(tradeEvent.RealTokenReserves), QuoteReserve: decimal.NewFromUint64(pumpQuoteReserve(tradeEvent)), Mayhem: tradeEvent.MayhemMode || isMayhemPump(pumpAccount(result, instruction, layout.FeeRecipient)), UserBaseBalance: userBase, UserQuoteBalance: userQuote, EntryContract: entryContract, Cashback: isCashbackCoin, }, } if swapMode, fixedAmount, limitAmount, ok := pumpTradeAmountInfoFromArgs(args); ok { swaps[0].SetSwapAmountInfo(swapMode, fixedAmount, limitAmount) normalizePumpQuoteSideMint(&swaps[0]) } if completed { swaps = append(swaps, Swap{ Program: SolProgramPump, Event: "complete", Pool: pumpAccount(result, instruction, layout.Pool), BaseMint: tradeEvent.Mint, QuoteMint: quoteMint, BaseTokenProgram: baseTokenProgram, QuoteTokenProgram: quoteTokenProgram, Creator: tradeEvent.Creator, BaseMintDecimals: 6, QuoteMintDecimals: pumpQuoteDecimals(result, quoteMint), User: user, BaseReserve: decimal.NewFromUint64(tradeEvent.RealTokenReserves), QuoteReserve: decimal.NewFromUint64(pumpQuoteReserve(tradeEvent)), Mayhem: tradeEvent.MayhemMode || isMayhemPump(pumpAccount(result, instruction, layout.FeeRecipient)), UserBaseBalance: userBase, UserQuoteBalance: userQuote, EntryContract: entryContract, }) } return swaps, offset, nil } type MigrateEvent struct { User solana.PublicKey Mint solana.PublicKey //Creator solana.PublicKey MintAmount uint64 SolAmount uint64 PoolMigrationFee uint64 //CreatorFee uint64 BondingCurve solana.PublicKey TimeStamp int64 Pool solana.PublicKey } type pumpMigrateAccountLayout struct { IsV2 bool BaseMint int QuoteMint int Pool int BasePoolToken int QuotePoolToken int User int BaseTokenProgram int QuoteTokenProgram int } func pumpMigrateLayout(instr Instruction) (pumpMigrateAccountLayout, bool) { if len(instr.Data) < 8 { return pumpMigrateAccountLayout{}, false } discriminator := instr.Data[:8] switch { case bytes.Equal(discriminator, pumpMigrateDiscriminator[:]): if len(instr.Accounts) <= 14 { return pumpMigrateAccountLayout{}, false } return pumpMigrateAccountLayout{ BaseMint: 2, QuoteMint: 14, Pool: 3, BasePoolToken: 4, QuotePoolToken: -1, User: 5, BaseTokenProgram: 7, QuoteTokenProgram: -1, }, true case bytes.Equal(discriminator, pumpMigrateV2Discriminator[:]): if len(instr.Accounts) <= 20 { return pumpMigrateAccountLayout{}, false } return pumpMigrateAccountLayout{ IsV2: true, BaseMint: 2, QuoteMint: 3, Pool: 4, BasePoolToken: 5, QuotePoolToken: 6, User: 7, BaseTokenProgram: 19, QuoteTokenProgram: 20, }, true default: return pumpMigrateAccountLayout{}, false } } func decimalFromUint64WithFallback(primary, fallback uint64) decimal.Decimal { if primary != 0 { return decimal.NewFromUint64(primary) } return decimal.NewFromUint64(fallback) } func MigrateParser(tx *Tx, instr Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { result := tx.rawTx var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] var err error programIndex := instr.ProgramIDIndex layout, ok := pumpMigrateLayout(instr) if !ok { return nil, increaseOffset(offset), fmt.Errorf("unknown pump migrate instruction account layout, offset, %d, %d", offset[0], offset[1]) } ammprogramIdx := 0 for i, b := range result.accountList { if b.Equals(pumpAmmProgram) { ammprogramIdx = i break } } if ammprogramIdx == 0 { return nil, increaseOffset(offset), fmt.Errorf("pump migrate, amm program id not found in account list, offset, %d, %d", offset[0], offset[1]) } var prefixLen = offset[1] inners, err := getInnerInstructions(innerInstructions, prefixLen) if err != nil { return nil, increaseOffset(offset), fmt.Errorf("pump migrate get inner instructions offset, %d, %d", offset[0], offset[1]) } var ( migrateEvent MigrateEvent createEvent ammCreatePoolEvent newoffset [2]uint ) for innerIndex, innerInstr := range inners { if innerInstr.ProgramIDIndex == ammprogramIdx && bytes.Equal(innerInstr.Data[:8], pumpAmmEventDiscriminator[:]) { if bytes.Equal(innerInstr.Data[8:16], pumpAmmCreateEventDiscriminator[:]) { err = agbinary.NewBorshDecoder(innerInstr.Data[16:]).Decode(&createEvent) if offset[1] == 0 { newoffset = [2]uint{offset[0] + 1, offset[1]} } else { newoffset = [2]uint{offset[0], prefixLen + uint(innerIndex) + 1} } if err != nil { return nil, newoffset, fmt.Errorf("pump amm createEvent decode error: %v, offset, %d, %d", err, offset[0], offset[1]) } } // } else if innerInstr.ProgramIDIndex == programIndex && bytes.Equal(innerInstr.Data[:8], pumpEventDiscriminator[:]) { if bytes.Equal(innerInstr.Data[8:16], pumpMigrateEventDiscriminator[:]) { err = agbinary.NewBorshDecoder(innerInstr.Data[16:]).Decode(&migrateEvent) if offset[1] == 0 { newoffset = [2]uint{offset[0] + 1, offset[1]} } else { newoffset = [2]uint{offset[0], prefixLen + uint(innerIndex) + 1} } if err != nil { return nil, newoffset, fmt.Errorf("pump buy event decode error: %v, offset, %d, %d", err, offset[0], offset[1]) } break } } } if migrateEvent == (MigrateEvent{}) || createEvent == (ammCreatePoolEvent{}) { offset = [2]uint{newoffset[0], newoffset[1]} return nil, increaseOffset(offset), InstructionIgnoredError } offset = [2]uint{newoffset[0], newoffset[1]} // verify migrate by checking create pool and migrate event userIndex := instr.Accounts[layout.User] ataBondingCurveAccountIndex := instr.Accounts[layout.BasePoolToken] bc, err := getTokenBalanceAfterTx(result, ataBondingCurveAccountIndex) if err != nil || bc == nil { return nil, increaseOffset(offset), fmt.Errorf("pump migrate get bonding curve balance error: %v, offset, %d, %d", err, offset[0], offset[1]) } baseTokenProgram := bc.ProgramIDAccount if layout.IsV2 { baseTokenProgram = pumpAccount(result, instr, layout.BaseTokenProgram) } quoteMint := createEvent.QuoteMint if quoteMint.IsZero() { quoteMint = pumpAccount(result, instr, layout.QuoteMint) } quoteTokenProgram := pumpAccount(result, instr, layout.QuoteTokenProgram) if quoteTokenProgram.IsZero() && !quoteMint.IsZero() { quoteTokenProgram = solana.TokenProgramID } quoteDecimals := createEvent.QuoteMintDecimals if quoteDecimals == 0 { quoteDecimals = pumpQuoteDecimals(result, quoteMint) } var userBase decimal.Decimal if result.accountList[userIndex].Equals(pumpMigrationAccount) { userBase = decimal.Zero } else { userBase = GetTokenBalanceAfterTx(result, userIndex, baseTokenProgram, migrateEvent.Mint) } userQuote := decimal.Zero if layout.IsV2 && !quoteMint.Equals(wSolMint) { userQuote = GetTokenBalanceAfterTx(result, userIndex, quoteTokenProgram, quoteMint) } else { userQuoteLamports, _ := GetSolAfterTx(result, userIndex) userQuote = decimal.NewFromUint64(userQuoteLamports) } baseAmount := decimalFromUint64WithFallback(createEvent.BaseAmountIn, migrateEvent.MintAmount) quoteAmount := decimalFromUint64WithFallback(createEvent.QuoteAmountIn, migrateEvent.SolAmount) baseReserve := decimalFromUint64WithFallback(createEvent.PoolBaseAmount, migrateEvent.MintAmount) quoteReserve := decimalFromUint64WithFallback(createEvent.PoolQuoteAmount, migrateEvent.SolAmount) if _, exists := tx.Token[migrateEvent.Mint]; !exists { tx.Token[migrateEvent.Mint] = TokenMeta{ Mint: migrateEvent.Mint, TokenProgram: baseTokenProgram, Decimals: 6, } } swaps := []Swap{ { Program: SolProgramPump, Event: "migrate", Pool: migrateEvent.BondingCurve, BaseMint: migrateEvent.Mint, QuoteMint: quoteMint, BaseTokenProgram: baseTokenProgram, QuoteTokenProgram: quoteTokenProgram, Creator: createEvent.Creator, BaseMintDecimals: 6, QuoteMintDecimals: quoteDecimals, User: migrateEvent.User, //BaseAmount: decimal.Decimal{}, //QuoteAmount: decimal.Decimal{}, BaseReserve: baseReserve, QuoteReserve: quoteReserve, Mayhem: createEvent.IsMayhemMode, MigrateTopProgram: pumpAmmProgram, MigrateToPool: migrateEvent.Pool, UserBaseBalance: userBase, UserQuoteBalance: userQuote, EntryContract: entryContract, }, } swaps = append(swaps, Swap{ Program: SolProgramPumpAMM, Event: "create", Pool: migrateEvent.Pool, BaseMint: migrateEvent.Mint, QuoteMint: quoteMint, BaseTokenProgram: baseTokenProgram, QuoteTokenProgram: quoteTokenProgram, Creator: createEvent.Creator, BaseMintDecimals: 6, QuoteMintDecimals: quoteDecimals, User: migrateEvent.User, BaseAmount: baseAmount, QuoteAmount: quoteAmount, BaseReserve: baseReserve, QuoteReserve: quoteReserve, Mayhem: createEvent.IsMayhemMode, UserBaseBalance: userBase, UserQuoteBalance: userQuote, EntryContract: entryContract, }) return swaps, offset, nil }