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 pumpBuyV2Discriminator, pumpBuyDiscriminator, pumpSellDiscriminator: 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: 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 } 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[:]) { err = agbinary.NewBorshDecoder(innerInstr.Data[16:]).Decode(&createEvent) 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) 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: solana.PublicKey{}, BaseTokenProgram: createEvent.TokenProgram, QuoteTokenProgram: solana.PublicKey{}, Creator: createEvent.Creator, BaseMintDecimals: 6, QuoteMintDecimals: 9, 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: decimal.NewFromUint64(userQuote), 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 } 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 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] user := result.accountList[instruction.Accounts[6]] ataUserIdx := instruction.Accounts[5] userIndex := instruction.Accounts[6] mint := result.accountList[instruction.Accounts[2]] 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 ( solAmount, tokenAmount uint64 ) if bytes.Equal(args.Discriminator[:], pumpBuyV2Discriminator[:]) { event = "buy_failed" solAmount = args.Amount1 tokenAmount = args.Amount2 } else if bytes.Equal(args.Discriminator[:], pumpBuyDiscriminator[:]) { event = "buy_failed" solAmount = args.Amount2 tokenAmount = args.Amount1 } else if bytes.Equal(args.Discriminator[:], pumpSellDiscriminator[:]) { event = "sell_failed" solAmount = args.Amount2 tokenAmount = args.Amount1 } else { return nil, increaseOffset(offset), fmt.Errorf("unknown pump trade instruction discriminator, offset, %d, %d", offset[0], offset[1]) } var baseTokenProgram solana.PublicKey if event == "buy_failed" { baseTokenProgram = result.accountList[instruction.Accounts[8]] } else { baseTokenProgram = result.accountList[instruction.Accounts[9]] } 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, _ := GetSolAfterTx(result, userIndex) bcIdx := instruction.Accounts[3] bcAtaIndex := instruction.Accounts[4] solReserves, _ := GetSolAfterTx(result, bcIdx) tokenReserves := getAccountBalanceAfterTx(result, bcAtaIndex) swaps := []Swap{ { Program: SolProgramPump, Event: event, Pool: result.accountList[instruction.Accounts[3]], BaseMint: mint, QuoteMint: solana.PublicKey{}, BaseTokenProgram: baseTokenProgram, QuoteTokenProgram: solana.PublicKey{}, BaseMintDecimals: 6, QuoteMintDecimals: 9, User: user, BaseAmount: decimal.NewFromUint64(tokenAmount), QuoteAmount: decimal.NewFromUint64(solAmount), BaseReserve: tokenReserves, QuoteReserve: decimal.NewFromUint64(solReserves), Mayhem: isMayhemPump(result.accountList[instruction.Accounts[1]]), UserBaseBalance: userBase, UserQuoteBalance: decimal.NewFromUint64(userQuote), EntryContract: entryContract, }, } 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 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 ) 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 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]) { err = agbinary.NewBorshDecoder(innerInstr.Data[16:]).Decode(&tradeEvent) 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]) } if !tradeEvent.IsBuy { break } } else if bytes.Equal(innerInstr.Data[8:16], pumpCompleteEventDiscriminator[:]) { err = agbinary.NewBorshDecoder(innerInstr.Data[16:]).Decode(&completeEvent) 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 completeEvent event decode error: %v, offset, %d, %d", err, offset[0], offset[1]) } completed = true break } } } if tradeEvent == (PumpTradeEvent{}) { 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]} event := "" baseTokenProgram := solana.TokenProgramID if tradeEvent.IsBuy { event = "buy" baseTokenProgram = result.accountList[instruction.Accounts[8]] } else { event = "sell" baseTokenProgram = result.accountList[instruction.Accounts[9]] } 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[5] userIndex := instruction.Accounts[6] 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, _ := GetSolAfterTx(result, userIndex) solAmount := tradeEvent.SolAmount if tradeEvent.IsBuy && bytes.Equal(instruction.Data[:8], pumpBuyV2Discriminator[:]) { fee := tradeEvent.Fee + tradeEvent.CreatorFee solAmount = tradeFeeArg.TradeSize if solAmount > fee { solAmount = solAmount - fee } } isCashbackCoin := tradeEvent.CashbackFeeBasisPoints > 0 || tradeEvent.Cashback > 0 swaps := []Swap{ { Program: SolProgramPump, Event: event, Pool: result.accountList[instruction.Accounts[3]], BaseMint: tradeEvent.Mint, QuoteMint: solana.PublicKey{}, BaseTokenProgram: baseTokenProgram, QuoteTokenProgram: solana.PublicKey{}, Creator: tradeEvent.Creator, BaseMintDecimals: 6, QuoteMintDecimals: 9, User: user, BaseAmount: decimal.NewFromUint64(tradeEvent.TokenAmount), QuoteAmount: decimal.NewFromUint64(solAmount), BaseReserve: decimal.NewFromUint64(tradeEvent.RealTokenReserves), QuoteReserve: decimal.NewFromUint64(tradeEvent.RealSolReserves), Mayhem: isMayhemPump(result.accountList[instruction.Accounts[1]]), UserBaseBalance: userBase, UserQuoteBalance: decimal.NewFromUint64(userQuote), EntryContract: entryContract, Cashback: isCashbackCoin, }, } if completed { swaps = append(swaps, Swap{ Program: SolProgramPump, Event: "complete", Pool: result.accountList[instruction.Accounts[3]], BaseMint: tradeEvent.Mint, QuoteMint: solana.PublicKey{}, BaseTokenProgram: result.accountList[instruction.Accounts[8]], QuoteTokenProgram: solana.PublicKey{}, Creator: tradeEvent.Creator, BaseMintDecimals: 6, QuoteMintDecimals: 9, User: user, BaseReserve: decimal.NewFromUint64(tradeEvent.RealTokenReserves), QuoteReserve: decimal.NewFromUint64(tradeEvent.RealSolReserves), Mayhem: isMayhemPump(result.accountList[instruction.Accounts[1]]), UserBaseBalance: userBase, UserQuoteBalance: decimal.NewFromUint64(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 } 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 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[5] ataBondingCurveAccountIndex := instr.Accounts[4] 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 var userBase decimal.Decimal if result.accountList[userIndex].Equals(pumpMigrationAccount) { userBase = decimal.Zero } else { userBase = GetTokenBalanceAfterTx(result, userIndex, baseTokenProgram, migrateEvent.Mint) } userQuote, _ := GetSolAfterTx(result, userIndex) 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: solana.PublicKey{}, BaseTokenProgram: baseTokenProgram, QuoteTokenProgram: solana.PublicKey{}, Creator: createEvent.Creator, BaseMintDecimals: 6, QuoteMintDecimals: 9, User: migrateEvent.User, //BaseAmount: decimal.Decimal{}, //QuoteAmount: decimal.Decimal{}, BaseReserve: decimal.NewFromUint64(migrateEvent.MintAmount), QuoteReserve: decimal.NewFromUint64(migrateEvent.SolAmount), Mayhem: createEvent.IsMayhemMode, MigrateTopProgram: pumpAmmProgram, MigrateToPool: migrateEvent.Pool, UserBaseBalance: userBase, UserQuoteBalance: decimal.NewFromUint64(userQuote), EntryContract: entryContract, }, } swaps = append(swaps, Swap{ Program: SolProgramPumpAMM, Event: "create", Pool: migrateEvent.Pool, BaseMint: migrateEvent.Mint, QuoteMint: wSolMint, BaseTokenProgram: baseTokenProgram, QuoteTokenProgram: solana.TokenProgramID, Creator: createEvent.Creator, BaseMintDecimals: 6, QuoteMintDecimals: 9, User: migrateEvent.User, BaseAmount: decimal.NewFromUint64(migrateEvent.MintAmount), QuoteAmount: decimal.NewFromUint64(migrateEvent.SolAmount), BaseReserve: decimal.NewFromUint64(migrateEvent.MintAmount), QuoteReserve: decimal.NewFromUint64(migrateEvent.SolAmount), Mayhem: createEvent.IsMayhemMode, UserBaseBalance: userBase, UserQuoteBalance: decimal.NewFromUint64(userQuote), EntryContract: entryContract, }) return swaps, offset, nil }