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(result *RawTx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { if !result.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 { offset[1] += 1 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: return BuyOrSellParser(result, instruction, innerInstructions, offset) case pumpCreateDiscriminator, pumpCreateV2Discriminator: return CreateParser(result, instruction, innerInstructions, offset) case pumpMigrateDiscriminator: return MigrateParser(result, 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 } func getInnerInstructions(innerInstructions InnerInstructions, offset uint) ([]Instruction, error) { var inners []Instruction var prefixLen = offset if prefixLen > uint(len(innerInstructions.Instructions)) { return nil, fmt.Errorf("error inner instruction index out of range") } if prefixLen == 0 { inners = innerInstructions.Instructions } else { inners = innerInstructions.Instructions[prefixLen:] } return inners, nil } func CreateParser(result *RawTx, instr Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { 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) 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, 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 } type CompleteEvent struct { User solana.PublicKey Mint solana.PublicKey BondingCurve solana.PublicKey Timestamp int64 } func BuyOrSellParser(result *RawTx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] var err error var programIndex = instruction.ProgramIDIndex var ( tradeEvent PumpTradeEvent 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]) } for innerIndex, innerInstr := range inners { 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]} ataUserIdx := instruction.Accounts[5] userIndex := instruction.Accounts[6] userBase := getAccountBalanceAfterTx(result, ataUserIdx) userQuote, _ := GetSolAfterTx(result, userIndex) event := "" baseTokenProgram := solana.TokenProgramID if tradeEvent.IsBuy { event = "buy" baseTokenProgram = result.accountList[instruction.Accounts[8]] } else { event = "sell" baseTokenProgram = result.accountList[instruction.Accounts[9]] } 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: tradeEvent.User, BaseAmount: decimal.NewFromUint64(tradeEvent.TokenAmount), QuoteAmount: decimal.NewFromUint64(tradeEvent.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, }, } 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: tradeEvent.User, 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(result *RawTx, instr Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { 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) 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, 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 }