package shreder import ( "bytes" "encoding/binary" "fmt" "math/big" "strings" "sync" "time" "github.com/gagliardetto/solana-go" "github.com/mr-tron/base58" "github.com/near/borsh-go" "github.com/shopspring/decimal" ) const ( wsolMint = "So11111111111111111111111111111111111111112" tokenProgram = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" ) // program ids var ( pumpProgramID = solana.MustPublicKeyFromBase58("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P") // has no sell function with pump and pump.amm program azczProgramID = solana.MustPublicKeyFromBase58("AzcZqCRUQgKEg5FTAgY7JacATABEYCEfMbjXEzspLYFB") // only buy function with pump program f5tfProgramID = solana.MustPublicKeyFromBase58("F5tfvbLog9VdGUPqBDTT8rgXvTTcq7e5UiGnupL1zvBq") // only pump.fun function photonProgramID = solana.MustPublicKeyFromBase58("BSfD6SHZigAfDWSjzD5Q41jw8LmKwtmjskPH9XW1mrRW") pumpAmmProgramID = solana.MustPublicKeyFromBase58("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA") boboProgramID = solana.MustPublicKeyFromBase58("BobogA5N2KN2GG4XN3E3rNNRw3L8H1QPXp7QLxGrNHGM") qtkvProgramID = solana.MustPublicKeyFromBase58("qtkvapJEvRWWrB7i5K6RaA1kvq5x3qmMKZ98ad71XQ7") // only buy function with pump program fjszProgramID = solana.MustPublicKeyFromBase58("FJsZbftBqRLfF7uqUKpm4s2goDr6xsQ5Q3mN7AFJB6hK") flasProgramID = solana.MustPublicKeyFromBase58("FLASHX8DrLbgeR8FcfNV1F5krxYcYMUdBkrP1EPBtxB9") terminalProgramID = solana.MustPublicKeyFromBase58("term9YPb9mzAsABaqN71A4xdbxHmpBNZavpBiQKZzN3") jupiterV6ProgramID = solana.MustPublicKeyFromBase58("JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4") gmgnProgramID = solana.MustPublicKeyFromBase58("GMgnVFR8Jb39LoXsEVzb3DvBy3ywCmdmJquHUy1Lrkqb") bonkProgramID = solana.MustPublicKeyFromBase58("BBRouter1cVunVXvkcqeKkZQcBK7ruan37PPm3xzWaXD") bloomRouterProgramID = solana.MustPublicKeyFromBase58("b1oomGGqPKGD6errbyfbVMBuzSC8WtAAYo8MwNafWW1") // For Metaora dlmm dlmmProgramID = solana.MustPublicKeyFromBase58("LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo") ) type AccountNotFoundError struct { Index int Len int } func NewAccountNotFoundError(i, l int) error { return &AccountNotFoundError{i, l} } func (e AccountNotFoundError) Error() string { return fmt.Sprintf("account index %d out of range, len=%d", e.Index, e.Len) } // instruction discriminators var ( pumpCreateCoinIX = []byte{24, 30, 200, 40, 5, 28, 7, 119} pumpCreateCoinV2IX = []byte{214, 144, 76, 236, 95, 139, 49, 180} pumpExtendedSellIX = []byte{51, 230, 133, 164, 1, 127, 131, 173} pumpBuyTokensIX = []byte{102, 6, 61, 18, 1, 218, 235, 234} pumpBuyV2TokensIX = []byte{56, 252, 116, 8, 158, 223, 205, 95} azczBuyTokensIX = []byte{11} azczAmmBuyTokensIX = []byte{0xf} f5tfBuyTokensIX = []byte{0} flasBuyTokensIX = []byte{0x00, 0x1, 0x4} flasSellTokensIX = []byte{0x01, 0x1, 0x3} flasAmmBuyTokensIX = []byte{0x00, 0x2, 0x2} flasAmmSellTokensIX = []byte{0x01, 0x2, 0x2} pumpAmmBuyTokensV2IX = []byte{198, 46, 21, 82, 180, 217, 232, 112} pumpAmmBuyTokensIX = []byte{102, 6, 61, 18, 1, 218, 235, 234} pumpAmmSellTokensIX = []byte{51, 230, 133, 164, 1, 127, 131, 173} qtkvBuyTokensIX = []byte{0x02} qtkvSellTokensIX = []byte{0x03} qtkvAmmSellTokensIX = []byte{0x05} boboBuyPumpTokensIX = []byte{0xff, 0xe7, 0x11, 0x53, 0x15, 0xc5, 0xc3, 0xdf} fjszBuyTokensIX = []byte{0xe7, 0x3f, 0x99, 0x83, 0xf3, 0xed, 0xe3, 0x3c} photonBuyPumpTokensIX = []byte{0x52, 0xe1, 0x77, 0xe7, 0x4e, 0x1d, 0x2d, 0x46} photonSwapPumpAmmIX = []byte{0x2c, 0x77, 0xaf, 0xda, 0xc7, 0x4d, 0xc4, 0xeb} terminalBuyTokensIX = []byte{0xa6, 0x54, 0x14, 0x96, 0x9f, 0x77, 0x59, 0xca} terminalSellTokensIX = []byte{0xbe, 0x84, 0xa2, 0x96, 0x93, 0x7c, 0xf8, 0x6b} terminalAmmSellTokensIX = []byte{0x40, 0x64, 0x97, 0xb9, 0x16, 0xfa, 0xec, 0xb1} gmgnBuyTokensIX = []byte{0x66, 0x06, 0x3d, 0x12, 0x01, 0xda, 0xeb, 0xea} bonkBuyAndSellTokensIX = []byte{0x00, 0x01, 0x00, 0x00, 0x00, 0x09, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a} dlmmSwapIX = []byte{248, 198, 158, 145, 225, 117, 135, 200} dlmmSwap2IX = []byte{65, 75, 63, 76, 235, 91, 91, 136} dlmmSwapExactOutIX = []byte{250, 73, 101, 33, 38, 207, 75, 184} dlmmSwapExactOut2IX = []byte{43, 215, 247, 132, 137, 60, 243, 81} dlmmSwapPriceImpactIX = []byte{56, 173, 230, 208, 173, 228, 156, 205} dlmmSwapPriceImpact2IX = []byte{74, 98, 192, 214, 177, 51, 75, 51} ) type compiledInstruction struct { ProgramIDIndex uint8 Accounts []uint8 Data []byte } type addressTableLookup struct { AccountKey solana.PublicKey WritableIndexes []uint8 ReadonlyIndexes []uint8 } type versionedMessage struct { StaticAccountKeys []solana.PublicKey Instructions []compiledInstruction AddressTableLookups []addressTableLookup } type versionedTransaction struct { Signatures []solana.Signature Message versionedMessage Block uint64 Time time.Time } type pumpExtendedSellArgs struct { Amount uint64 MinSolOutput uint64 } type pumpBuyArgs struct { Amount uint64 MaxSolCost uint64 } type pumpCreateCoinV2Args struct { Name string Symbol string Uri string Creator solana.PublicKey IsMayhemMode bool } type azczBuyArgs struct { SolAmount uint64 TokenAmount uint64 } type f5tfBuyArgs struct { SolAmount uint64 TokenAmount uint64 } type flasArgs struct { Amount1 uint64 Amount2 uint64 Placeholder [3]uint8 } type photonBuyPumpArgs struct { Timestamp uint64 SolAmount uint64 TokenAmount uint64 Fee uint64 } type photonSwapPumpAmmArgs struct { FromAmount uint64 ToAmount uint64 } type bloomRouterArgs struct { Side uint16 SolAmount uint64 TokenAmount uint64 } type pumpAmmBuyArgs struct { Amount uint64 MaxSolCost uint64 } type boboBuyArgs struct { Placeholder1 uint64 Placeholder2 uint64 SolAmount uint64 Placeholder3 uint64 Placeholder4 uint64 Placeholder5 uint64 Placeholder6 uint64 } type qtkvBuyArgs struct { Placeholder uint64 TokenNumber uint64 SolAmount uint64 } type fjszBuyArgs struct { SolAmount uint64 TokenAmount uint64 } var ( versionedPool = sync.Pool{} accIdxPool = sync.Pool{} ) func requireAccIdxSlice() []uint8 { v := accIdxPool.Get() if v == nil { return make([]uint8, 0, 16) } return v.([]uint8) } func releaseAccIdxSlice(s []uint8) { if s == nil { return } s = s[:0] accIdxPool.Put(s) } func requireVersionedPool() *versionedTransaction { v := versionedPool.Get() if v == nil { return &versionedTransaction{ Signatures: make([]solana.Signature, 0, 10), Message: versionedMessage{ StaticAccountKeys: make([]solana.PublicKey, 0, 256), Instructions: make([]compiledInstruction, 0, 16), AddressTableLookups: make([]addressTableLookup, 0, 10), }, } } return v.(*versionedTransaction) } func releaseVersionedPool(v *versionedTransaction) { if v == nil { return } for i := range v.Message.Instructions { releaseAccIdxSlice(v.Message.Instructions[i].Accounts) } for i := range v.Message.AddressTableLookups { releaseAccIdxSlice(v.Message.AddressTableLookups[i].WritableIndexes) releaseAccIdxSlice(v.Message.AddressTableLookups[i].ReadonlyIndexes) } versionedPool.Put(v) } // ParseTransaction mirrors the Rust parse_transaction entry point. func ParseTransaction(update *SubscribeUpdateTransaction, loader *AddressTables, stats bool) []*TxSignal { var now time.Time if stats { now = time.Now() } versioned, err := toVersionedTransaction(update) if err != nil || versioned == nil || len(versioned.Signatures) == 0 { return nil } defer func() { releaseVersionedPool(versioned) }() txHash := versioned.Signatures[0] // staticKeys := versioned.Message.StaticAccountKeys instructions := versioned.Message.Instructions if loader != nil && len(versioned.Message.AddressTableLookups) > 0 { lookupTableOk := true for _, lookup := range versioned.Message.AddressTableLookups { if len(lookup.WritableIndexes) == 0 { continue } lookupTableOk = loader.FillToTx(versioned, lookup.AccountKey, lookup.WritableIndexes) if !lookupTableOk { break } } if lookupTableOk { for _, lookup := range versioned.Message.AddressTableLookups { if len(lookup.ReadonlyIndexes) == 0 { continue } lookupTableOk = loader.FillToTx(versioned, lookup.AccountKey, lookup.ReadonlyIndexes) if !lookupTableOk { break } } } // versioned.Message.StaticAccountKeys = staticKeys } var parsed []*TxSignal = make([]*TxSignal, 0, 3) for i := range instructions { inst := instructions[i] if int(inst.ProgramIDIndex) >= len(versioned.Message.StaticAccountKeys) { continue } programID := versioned.Message.StaticAccountKeys[inst.ProgramIDIndex] switch programID { case pumpProgramID: txRes, err := parsePumpInstruction(versioned, i) parsed = appendParsed(now, parsed, txRes, err, txHash, "pump") case azczProgramID: txRes, err := parseAzczInstruction(versioned, i) parsed = appendParsed(now, parsed, txRes, err, txHash, "azcz") case f5tfProgramID: txRes, err := parseF5tfInstruction(versioned, i) parsed = appendParsed(now, parsed, txRes, err, txHash, "f5tf") case flasProgramID: txRes, err := parseFlasInstruction(versioned, i) parsed = appendParsed(now, parsed, txRes, err, txHash, "flas") case photonProgramID: txRes, err := parsePhotonInstruction(versioned, i) parsed = appendParsed(now, parsed, txRes, err, txHash, "photon") case pumpAmmProgramID: txRes, err := parsePumpAmmInstruction(versioned, i) parsed = appendParsed(now, parsed, txRes, err, txHash, "pumpamm") case boboProgramID: txRes, err := parseBoboInstruction(versioned, i) parsed = appendParsed(now, parsed, txRes, err, txHash, "bobo") case qtkvProgramID: txRes, err := parseQtkvInstruction(versioned, i) parsed = appendParsed(now, parsed, txRes, err, txHash, "qtkv") case fjszProgramID: txRes, err := parseFjszInstruction(versioned, i) parsed = appendParsed(now, parsed, txRes, err, txHash, "fjsz") case terminalProgramID: txRes, err := parseTermInstruction(versioned, i) parsed = appendParsed(now, parsed, txRes, err, txHash, "terminal") case jupiterV6ProgramID: txRes, err := parseJupiterV6Instruction(versioned, i) parsed = appendParsed(now, parsed, txRes, err, txHash, "jupiterv6") case okxDexRouteV2ProgramID: txRes, err := parseOkxDexRouteV2Instruction(versioned, i) parsed = appendParsed(now, parsed, txRes, err, txHash, "okxdexroutev2") case dflowProgramID: txRes, err := parseDFlowInstruction(versioned, i) parsed = appendParsed(now, parsed, txRes, err, txHash, "dflow") case gmgnProgramID: txRes, err := parseGMGNInstruction(versioned, i) parsed = appendParsed(now, parsed, txRes, err, txHash, "gmgn") case bonkProgramID: txRes, err := parseBonkInstruction(versioned, i) parsed = appendParsed(now, parsed, txRes, err, txHash, "bonk") case bloomRouterProgramID: txRes, err := parseBloomRouterInstruction(versioned, i) parsed = appendParsed(now, parsed, txRes, err, txHash, "bloomrouter") case dlmmProgramID: txRes, err := parseDlmmInstruction(versioned, i) parsed = appendParsed(now, parsed, txRes, err, txHash, "dlmm") } } return parsed } func appendParsed(start time.Time, list []*TxSignal, parsed *TxSignal, err error, txHash [64]byte, label string) []*TxSignal { if err != nil { if !strings.HasPrefix(err.Error(), "account index") { logger.Debug("txparser: failed to parse", "label", label, "instruction", err, "tx_hash", base58.Encode(txHash[:])) } return list } if parsed != nil { parsed.Label = label if !start.IsZero() { parsed.ParseEnd = time.Now() parsed.ParseStart = start } list = append(list, parsed) } return list } func toVersionedTransaction(update *SubscribeUpdateTransaction) (*versionedTransaction, error) { if update == nil || update.Transaction == nil || update.Transaction.Message == nil { return nil, fmt.Errorf("transaction is nil") } protoTx := update.Transaction msg := protoTx.Message versioned := requireVersionedPool() versioned.Signatures = versioned.Signatures[:0] for _, rawSig := range protoTx.Signatures { versioned.Signatures = append(versioned.Signatures, solana.SignatureFromBytes(rawSig)) } versioned.Message.StaticAccountKeys = versioned.Message.StaticAccountKeys[:0] for _, key := range msg.AccountKeys { versioned.Message.StaticAccountKeys = append(versioned.Message.StaticAccountKeys, solana.PublicKeyFromBytes(key)) } versioned.Message.Instructions = versioned.Message.Instructions[:0] for _, instr := range msg.Instructions { accounts := requireAccIdxSlice() accounts = append(accounts, instr.Accounts...) versioned.Message.Instructions = append(versioned.Message.Instructions, compiledInstruction{ ProgramIDIndex: uint8(instr.ProgramIdIndex), Accounts: accounts, Data: instr.Data, }) } versioned.Message.AddressTableLookups = versioned.Message.AddressTableLookups[:0] for _, lookup := range msg.AddressTableLookups { writable := requireAccIdxSlice() writable = append(writable, lookup.WritableIndexes...) readonly := requireAccIdxSlice() readonly = append(readonly, lookup.ReadonlyIndexes...) versioned.Message.AddressTableLookups = append(versioned.Message.AddressTableLookups, addressTableLookup{ AccountKey: solana.PublicKeyFromBytes(lookup.AccountKey), WritableIndexes: writable, ReadonlyIndexes: readonly, }) } versioned.Block = update.GetSlot() return versioned, nil } func formatTokenAmount(amount uint64) decimal.Decimal { val := decimal.NewFromBigInt(new(big.Int).SetUint64(amount), 0) return val.Div(decimal.NewFromInt(1_000_000)) } func formatSolAmount(lamports uint64) decimal.Decimal { val := decimal.NewFromBigInt(new(big.Int).SetUint64(lamports), 0) return val.Div(decimal.NewFromInt(1_000_000_000)) } func getStaticKey(static []solana.PublicKey, index int) (solana.PublicKey, error) { if index < 0 || index >= len(static) { return solana.PublicKey{}, NewAccountNotFoundError(index, len(static)) } return static[index], nil } func parsePumpInstruction(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) { msg := tx.Message if instructionIndex >= len(msg.Instructions) { return nil, fmt.Errorf("instruction index out of bounds") } instruction := msg.Instructions[instructionIndex] if len(instruction.Data) < 8 { return nil, fmt.Errorf("data is empty") } if matchMethod(instruction.Data[0:8], pumpBuyV2TokensIX) || matchMethod(instruction.Data[0:8], pumpBuyTokensIX) { return parsePumpBuy(tx, &instruction) } else if matchMethod(instruction.Data[0:8], pumpExtendedSellIX) { return parsePumpSell(tx, &instruction) } else if matchMethod(instruction.Data[0:8], pumpCreateCoinIX) { return parsePumpCreate(tx, &instruction) } else if matchMethod(instruction.Data[0:8], pumpCreateCoinV2IX) { return parsePumpCreateV2(tx, &instruction) } return nil, nil } func parsePumpCreate(tx *versionedTransaction, instruction *compiledInstruction) (*TxSignal, error) { if len(instruction.Accounts) < 8 { return nil, fmt.Errorf("accounts too short") } staticKeys := tx.Message.StaticAccountKeys mint, err := getStaticKey(staticKeys, int(instruction.Accounts[0])) if err != nil { return nil, err } creator, err := getStaticKey(staticKeys, int(instruction.Accounts[7])) if err != nil { return nil, err } return &TxSignal{ TxHash: tx.Signatures[0].String(), Label: "pump", Maker: creator.String(), Token0Address: mint.String(), Token1Address: wsolMint, Token0Amount: decimal.Zero, Token1Amount: decimal.Zero, Program: "Pump", Event: "create", IsToken2022: false, IsMayhemMode: false, Block: tx.Block, Token0AmountUint64: 0, Token1AmountUint64: 0, }, nil } func parsePumpCreateV2(tx *versionedTransaction, instruction *compiledInstruction) (*TxSignal, error) { if len(instruction.Accounts) < 8 { return nil, fmt.Errorf("accounts too short") } if len(instruction.Data) < 8 { return nil, fmt.Errorf("data too short for pump create v2 args, len=%d", len(instruction.Data)) } staticKeys := tx.Message.StaticAccountKeys mint, err := getStaticKey(staticKeys, int(instruction.Accounts[0])) if err != nil { return nil, err } tokenProgramKey, err := getStaticKey(staticKeys, int(instruction.Accounts[7])) if err != nil { return nil, err } var args pumpCreateCoinV2Args if err := borsh.Deserialize(&args, instruction.Data[8:]); err != nil { return nil, fmt.Errorf("failed to parse create coin v2 args: %w", err) } return &TxSignal{ TxHash: tx.Signatures[0].String(), Label: "pump", Maker: args.Creator.String(), Token0Address: mint.String(), Token1Address: wsolMint, Token0Amount: decimal.Zero, Token1Amount: decimal.Zero, Program: "Pump", Event: "create", IsToken2022: tokenProgramKey.String() != tokenProgram, IsMayhemMode: args.IsMayhemMode, Block: tx.Block, Token0AmountUint64: 0, Token1AmountUint64: 0, }, nil } func decodePumpBuyArgs(data []byte) (uint64, uint64, error) { if len(data) < 9 { return 0, 0, fmt.Errorf("data too short for pump buy buy args, len=%d", len(data)) } var args pumpBuyArgs if err := borsh.Deserialize(&args, data[8:]); err == nil { return args.Amount, args.MaxSolCost, nil } if len(data) >= 24 { amount := binary.LittleEndian.Uint64(data[8:16]) maxSol := binary.LittleEndian.Uint64(data[16:24]) return amount, maxSol, nil } return 0, 0, fmt.Errorf("failed to parse buy tokens args") } func parsePumpBuy(tx *versionedTransaction, instruction *compiledInstruction) (*TxSignal, error) { amount, sol, err := decodePumpBuyArgs(instruction.Data) if err != nil { return nil, err } exactIn := false if matchMethod(instruction.Data, pumpBuyV2TokensIX) { temp := amount amount = sol sol = temp exactIn = true } if len(instruction.Accounts) < 7 { return nil, fmt.Errorf("accounts too short") } staticKeys := tx.Message.StaticAccountKeys mint, err := getStaticKey(staticKeys, int(instruction.Accounts[2])) if err != nil { return nil, err } buyer, err := getStaticKey(staticKeys, int(instruction.Accounts[6])) if err != nil { return nil, err } return &TxSignal{ TxHash: tx.Signatures[0].String(), Label: "pump", Maker: buyer.String(), Token0Address: mint.String(), Token1Address: wsolMint, Token0Amount: formatTokenAmount(amount), Token1Amount: formatSolAmount(sol), Program: "Pump", Event: "buy", ExactSOL: exactIn, IsToken2022: false, IsMayhemMode: false, Block: tx.Block, Token0AmountUint64: amount, Token1AmountUint64: sol, }, nil } func decodePumpSellArgs(data []byte) (uint64, uint64, error) { if len(data) < 9 { return 0, 0, fmt.Errorf("data too short for pump sell sell args, len=%d", len(data)) } var args pumpExtendedSellArgs if err := borsh.Deserialize(&args, data[8:]); err == nil { return args.Amount, args.MinSolOutput, nil } if len(data) >= 24 { amount := binary.LittleEndian.Uint64(data[8:16]) minSol := binary.LittleEndian.Uint64(data[16:24]) return amount, minSol, nil } return 0, 0, fmt.Errorf("failed to parse sell tokens args") } func parsePumpSell(tx *versionedTransaction, instruction *compiledInstruction) (*TxSignal, error) { amount, minSol, err := decodePumpSellArgs(instruction.Data) if err != nil { return nil, err } if len(instruction.Accounts) < 7 { return nil, fmt.Errorf("accounts too short") } staticKeys := tx.Message.StaticAccountKeys mint, err := getStaticKey(staticKeys, int(instruction.Accounts[2])) if err != nil { return nil, err } seller, err := getStaticKey(staticKeys, int(instruction.Accounts[6])) if err != nil { return nil, err } return &TxSignal{ TxHash: tx.Signatures[0].String(), Label: "pump", Maker: seller.String(), Token0Address: mint.String(), Token1Address: wsolMint, Token0Amount: formatTokenAmount(amount), Token1Amount: formatSolAmount(minSol), Program: "Pump", Event: "sell", IsToken2022: false, IsMayhemMode: false, Block: tx.Block, Token0AmountUint64: amount, Token1AmountUint64: minSol, }, nil } func parseAzczInstruction(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) { msg := tx.Message if instructionIndex >= len(msg.Instructions) { return nil, fmt.Errorf("instruction index out of bounds") } instruction := msg.Instructions[instructionIndex] if len(instruction.Data) == 0 { return nil, fmt.Errorf("data is empty") } if matchMethod(instruction.Data, azczBuyTokensIX) { return parseAzczBuy(tx, instructionIndex) } else if matchMethod(instruction.Data, azczAmmBuyTokensIX) { return parseAzczAmmBuy(tx, instructionIndex) } return nil, nil } func parseAzczAmmBuy(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) { instruction := tx.Message.Instructions[instructionIndex] if len(instruction.Accounts) < 8 { return nil, fmt.Errorf("accounts too short") } staticKeys := tx.Message.StaticAccountKeys mint, err := getStaticKey(staticKeys, int(instruction.Accounts[3])) if err != nil { return nil, err } user, err := getStaticKey(staticKeys, int(instruction.Accounts[1])) if err != nil { return nil, err } if len(instruction.Data) < 17 { return nil, fmt.Errorf("data too short for azcz amm buy args, len=%d", len(instruction.Data)) } solAmount := binary.LittleEndian.Uint64(instruction.Data[1:9]) return &TxSignal{ TxHash: tx.Signatures[0].String(), Label: "azcz", Maker: user.String(), Token0Address: mint.String(), Token1Address: wsolMint, Token0Amount: decimal.Zero, Token1Amount: formatSolAmount(solAmount), Program: "Pump", Event: "buy", IsToken2022: false, IsMayhemMode: false, ExactSOL: true, Block: tx.Block, Token0AmountUint64: 0, Token1AmountUint64: solAmount, }, nil } func parseAzczBuy(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) { instruction := tx.Message.Instructions[instructionIndex] if len(instruction.Accounts) < 8 { return nil, fmt.Errorf("accounts too short") } staticKeys := tx.Message.StaticAccountKeys mint, err := getStaticKey(staticKeys, int(instruction.Accounts[3])) if err != nil { return nil, err } user, err := getStaticKey(staticKeys, int(instruction.Accounts[7])) if err != nil { return nil, err } if len(instruction.Data) < 2 { return nil, fmt.Errorf("data too short for azcz buy args len=%d", len(instruction.Data)) } var args azczBuyArgs if err := borsh.Deserialize(&args, instruction.Data[1:]); err != nil { return nil, fmt.Errorf("failed to parse buy tokens args: %w", err) } return &TxSignal{ TxHash: tx.Signatures[0].String(), Label: "azcz", Maker: user.String(), Token0Address: mint.String(), Token1Address: wsolMint, Token0Amount: formatTokenAmount(args.TokenAmount), Token1Amount: formatSolAmount(args.SolAmount), Program: "Pump", Event: "buy", IsToken2022: false, IsMayhemMode: false, Block: tx.Block, Token0AmountUint64: args.TokenAmount, Token1AmountUint64: args.SolAmount, }, nil } func parseF5tfInstruction(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) { msg := tx.Message if instructionIndex >= len(msg.Instructions) { return nil, fmt.Errorf("instruction index out of bounds") } instruction := msg.Instructions[instructionIndex] if len(instruction.Data) == 0 { return nil, fmt.Errorf("data is empty") } if !matchMethod(instruction.Data, f5tfBuyTokensIX) { return nil, nil } if len(instruction.Accounts) < 7 { return nil, fmt.Errorf("accounts too short") } staticKeys := msg.StaticAccountKeys mint, err := getStaticKey(staticKeys, int(instruction.Accounts[2])) if err != nil { return nil, err } user, err := getStaticKey(staticKeys, int(instruction.Accounts[6])) if err != nil { return nil, err } if len(instruction.Data) < 2 { return nil, fmt.Errorf("data too short for f5tf buy args len=%d", len(instruction.Data)) } var args f5tfBuyArgs if err := borsh.Deserialize(&args, instruction.Data[1:]); err != nil { return nil, fmt.Errorf("failed to parse buy tokens args: %w", err) } return &TxSignal{ TxHash: tx.Signatures[0].String(), Label: "f5tf", Maker: user.String(), Token0Address: mint.String(), Token1Address: wsolMint, Token0Amount: formatTokenAmount(args.TokenAmount), Token1Amount: formatSolAmount(args.SolAmount), Program: "Pump", Event: "buy", IsToken2022: false, IsMayhemMode: false, Block: tx.Block, Token0AmountUint64: args.TokenAmount, Token1AmountUint64: args.SolAmount, }, nil } func parseFlasInstruction(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) { msg := tx.Message if instructionIndex >= len(msg.Instructions) { return nil, fmt.Errorf("instruction index out of bounds") } instruction := msg.Instructions[instructionIndex] if len(instruction.Data) == 0 { return nil, fmt.Errorf("data is empty") } if len(instruction.Data) == 10 && instruction.Data[0] == 1 { return nil, nil } if len(instruction.Data) < 20 { return nil, fmt.Errorf("data too short for args flas instruction, len: %d", len(instruction.Data)) } methodData := instruction.Data[17:20] //if !matchMethod(methodData, flasBuyTokensIX) { // return nil, nil //} if matchMethod(methodData, flasBuyTokensIX) { return parseFlasBuy(tx, instructionIndex) } else if matchMethod(methodData, flasSellTokensIX) { return parseFlasSell(tx, instructionIndex) } else if matchMethod(methodData, flasAmmBuyTokensIX) { return parseFlasAmmBuy(tx, instructionIndex) } else if matchMethod(methodData, flasAmmSellTokensIX) { return parseFlasAmmSell(tx, instructionIndex) } return nil, nil } func parseFlasAmmSell(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) { instruction := tx.Message.Instructions[instructionIndex] if len(instruction.Accounts) < 10 { return nil, fmt.Errorf("accounts too short") } staticKeys := tx.Message.StaticAccountKeys mint, err := getStaticKey(staticKeys, int(instruction.Accounts[9])) if err != nil { return nil, err } user, err := getStaticKey(staticKeys, int(instruction.Accounts[1])) if err != nil { return nil, err } var args flasArgs if err := borsh.Deserialize(&args, instruction.Data[1:]); err != nil { return nil, fmt.Errorf("failed to parse buy tokens args: %w", err) } return &TxSignal{ TxHash: tx.Signatures[0].String(), Label: "flas", Maker: user.String(), Token0Address: mint.String(), Token1Address: wsolMint, Token0Amount: formatTokenAmount(args.Amount1), Token1Amount: formatSolAmount(args.Amount2), Program: "PumpAMM", Event: "sell", IsToken2022: false, IsMayhemMode: false, ExactSOL: false, Block: tx.Block, Token0AmountUint64: args.Amount1, Token1AmountUint64: args.Amount2, }, nil } func parseFlasAmmBuy(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) { instruction := tx.Message.Instructions[instructionIndex] if len(instruction.Accounts) < 10 { return nil, fmt.Errorf("accounts too short") } staticKeys := tx.Message.StaticAccountKeys mint, err := getStaticKey(staticKeys, int(instruction.Accounts[9])) if err != nil { return nil, err } user, err := getStaticKey(staticKeys, int(instruction.Accounts[1])) if err != nil { return nil, err } var args flasArgs if err := borsh.Deserialize(&args, instruction.Data[1:]); err != nil { return nil, fmt.Errorf("failed to parse buy tokens args: %w", err) } return &TxSignal{ TxHash: tx.Signatures[0].String(), Label: "flas", Maker: user.String(), Token0Address: mint.String(), Token1Address: wsolMint, Token0Amount: decimal.Zero, Token1Amount: formatSolAmount(args.Amount1), Program: "PumpAMM", Event: "buy", IsToken2022: false, IsMayhemMode: false, ExactSOL: true, Block: tx.Block, Token0AmountUint64: 0, Token1AmountUint64: args.Amount1, }, nil } func parseFlasSell(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) { instruction := tx.Message.Instructions[instructionIndex] if len(instruction.Accounts) < 9 { return nil, fmt.Errorf("accounts too short") } staticKeys := tx.Message.StaticAccountKeys mint, err := getStaticKey(staticKeys, int(instruction.Accounts[8])) if err != nil { return nil, err } user, err := getStaticKey(staticKeys, int(instruction.Accounts[1])) if err != nil { return nil, err } var args flasArgs if err := borsh.Deserialize(&args, instruction.Data[1:]); err != nil { return nil, fmt.Errorf("failed to parse buy tokens args: %w", err) } return &TxSignal{ TxHash: tx.Signatures[0].String(), Label: "flas", Maker: user.String(), Token0Address: mint.String(), Token1Address: wsolMint, Token0Amount: formatTokenAmount(args.Amount1), Token1Amount: formatSolAmount(args.Amount2), Program: "Pump", Event: "sell", IsToken2022: false, IsMayhemMode: false, Block: tx.Block, Token0AmountUint64: args.Amount1, Token1AmountUint64: args.Amount2, }, nil } func parseFlasBuy(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) { instruction := tx.Message.Instructions[instructionIndex] if len(instruction.Accounts) < 9 { return nil, fmt.Errorf("accounts too short") } staticKeys := tx.Message.StaticAccountKeys mint, err := getStaticKey(staticKeys, int(instruction.Accounts[8])) if err != nil { return nil, err } user, err := getStaticKey(staticKeys, int(instruction.Accounts[0])) if err != nil { return nil, err } if len(instruction.Data) > 20 { instruction.Data = instruction.Data[:20] } var args flasArgs if err := borsh.Deserialize(&args, instruction.Data[1:]); err != nil { return nil, fmt.Errorf("failed to parse buy tokens args: %w", err) } return &TxSignal{ TxHash: tx.Signatures[0].String(), Label: "flas", Maker: user.String(), Token0Address: mint.String(), Token1Address: wsolMint, Token0Amount: formatTokenAmount(args.Amount2), Token1Amount: formatSolAmount(args.Amount1), Program: "Pump", Event: "buy", IsToken2022: false, IsMayhemMode: false, ExactSOL: true, Block: tx.Block, Token0AmountUint64: args.Amount2, Token1AmountUint64: args.Amount1, }, nil } func parseGMGNInstruction(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) { msg := tx.Message if instructionIndex >= len(msg.Instructions) { return nil, fmt.Errorf("instruction index out of bounds") } instruction := msg.Instructions[instructionIndex] if len(instruction.Data) == 0 { return nil, fmt.Errorf("data is empty") } if len(instruction.Data) < 8 { return nil, nil } if matchMethod(instruction.Data, gmgnBuyTokensIX) { return parseGMGNBuy(tx, &instruction) } return nil, nil } func parseGMGNBuy(tx *versionedTransaction, instruction *compiledInstruction) (*TxSignal, error) { if len(instruction.Accounts) < 8 { return nil, fmt.Errorf("accounts too short") } if len(instruction.Data) < 24 { return nil, fmt.Errorf("data too short for gmgn buy args, len=%d", len(instruction.Data)) } staticKeys := tx.Message.StaticAccountKeys mint, err := getStaticKey(staticKeys, int(instruction.Accounts[2])) if err != nil { return nil, err } user, err := getStaticKey(staticKeys, int(instruction.Accounts[6])) if err != nil { return nil, err } solAmount := binary.LittleEndian.Uint64(instruction.Data[8:16]) tokenAmount := binary.LittleEndian.Uint64(instruction.Data[16:24]) return &TxSignal{ TxHash: tx.Signatures[0].String(), Label: "gmgn", Maker: user.String(), Token0Address: mint.String(), Token1Address: wsolMint, Token0Amount: formatTokenAmount(tokenAmount), Token1Amount: formatSolAmount(solAmount), Program: "Pump", Event: "buy", IsToken2022: false, IsMayhemMode: false, ExactSOL: true, Block: tx.Block, Token0AmountUint64: tokenAmount, Token1AmountUint64: solAmount, }, nil } func parsePhotonInstruction(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) { msg := tx.Message if instructionIndex >= len(msg.Instructions) { return nil, fmt.Errorf("instruction index out of bounds") } instruction := msg.Instructions[instructionIndex] if len(instruction.Data) == 0 { return nil, fmt.Errorf("data is empty") } if len(instruction.Data) < 8 { return nil, nil } switch { case bytes.Equal(instruction.Data[:8], photonBuyPumpTokensIX): return parsePhotonBuy(tx, &instruction) case bytes.Equal(instruction.Data[:8], photonSwapPumpAmmIX): return parsePhotonSwap(tx, &instruction) default: return nil, nil } } func parsePhotonBuy(tx *versionedTransaction, instruction *compiledInstruction) (*TxSignal, error) { if len(instruction.Accounts) < 8 { return nil, fmt.Errorf("accounts too short") } if len(instruction.Data) < 16 { return nil, fmt.Errorf("data too short for photon buy args, len=%d", len(instruction.Data)) } staticKeys := tx.Message.StaticAccountKeys mint, err := getStaticKey(staticKeys, int(instruction.Accounts[3])) if err != nil { return nil, err } user, err := getStaticKey(staticKeys, int(instruction.Accounts[7])) if err != nil { return nil, err } var args photonBuyPumpArgs if err := borsh.Deserialize(&args, instruction.Data[8:]); err != nil { return nil, fmt.Errorf("failed to parse buy tokens args: %w", err) } solAmount := args.SolAmount * (100000000 - 1234568) / 100000000 return &TxSignal{ TxHash: tx.Signatures[0].String(), Label: "photon", Maker: user.String(), Token0Address: mint.String(), Token1Address: wsolMint, Token0Amount: formatTokenAmount(args.TokenAmount), Token1Amount: formatSolAmount(solAmount), Program: "Pump", Event: "buy", IsToken2022: false, IsMayhemMode: false, ExactSOL: true, Block: tx.Block, Token0AmountUint64: args.TokenAmount, Token1AmountUint64: solAmount, }, nil } func parsePhotonSwap(tx *versionedTransaction, instruction *compiledInstruction) (*TxSignal, error) { if len(instruction.Accounts) < 8 { return nil, fmt.Errorf("accounts too short") } if len(instruction.Data) < 16 { return nil, fmt.Errorf("data too short for swap args for photon. len=%d", len(instruction.Data)) } staticKeys := tx.Message.StaticAccountKeys base, err := getStaticKey(staticKeys, int(instruction.Accounts[3])) if err != nil { return nil, err } quote, err := getStaticKey(staticKeys, int(instruction.Accounts[4])) if err != nil { return nil, err } if !quote.Equals(solana.WrappedSol) { return nil, nil } buyer, err := getStaticKey(staticKeys, int(instruction.Accounts[1])) if err != nil { return nil, err } var args photonSwapPumpAmmArgs if err := borsh.Deserialize(&args, instruction.Data[8:]); err != nil { return nil, fmt.Errorf("failed to parse swap pump amm tokens args: %w", err) } if args.FromAmount > args.ToAmount { // sell; ignore return nil, nil } solAmount := args.FromAmount * (100000000 - 1234568) / 100000000 return &TxSignal{ TxHash: tx.Signatures[0].String(), Label: "photon", Maker: buyer.String(), Token0Address: base.String(), Token1Address: wsolMint, Token0Amount: formatTokenAmount(args.ToAmount), Token1Amount: formatSolAmount(solAmount), Program: "PumpAMM", Event: "buy", IsToken2022: false, IsMayhemMode: false, Block: tx.Block, Token0AmountUint64: args.ToAmount, Token1AmountUint64: solAmount, }, nil } func parsePumpAmmInstruction(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) { msg := tx.Message if instructionIndex >= len(msg.Instructions) { return nil, fmt.Errorf("instruction index out of bounds") } instruction := msg.Instructions[instructionIndex] if len(instruction.Data) == 0 { return nil, fmt.Errorf("data is empty") } if matchMethod(instruction.Data, pumpAmmBuyTokensIX) || matchMethod(instruction.Data, pumpAmmBuyTokensV2IX) { return parsePumpAmmBuy(tx, &instruction) } else if matchMethod(instruction.Data, pumpAmmSellTokensIX) { return parsePumpAmmSell(tx, &instruction) } return nil, nil } func parseTermInstruction(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) { msg := tx.Message if instructionIndex >= len(msg.Instructions) { return nil, fmt.Errorf("instruction index out of bounds") } instruction := msg.Instructions[instructionIndex] if len(instruction.Data) == 0 { return nil, fmt.Errorf("data is empty") } if len(instruction.Data) < 24 { return nil, nil } switch { case bytes.Equal(instruction.Data[:8], terminalBuyTokensIX): return parseTermBuy(tx, &instruction) case bytes.Equal(instruction.Data[:8], terminalSellTokensIX): return parseTermSell(tx, &instruction) case bytes.Equal(instruction.Data[:8], terminalAmmSellTokensIX): return parseTermAmmSell(tx, &instruction) default: return nil, nil } } func parseTermAmmSell(tx *versionedTransaction, instruction *compiledInstruction) (*TxSignal, error) { if len(instruction.Accounts) < 8 { return nil, fmt.Errorf("accounts too short") } staticKeys := tx.Message.StaticAccountKeys mint, err := getStaticKey(staticKeys, int(instruction.Accounts[3])) if err != nil { return nil, err } user, err := getStaticKey(staticKeys, int(instruction.Accounts[1])) if err != nil { return nil, err } solAmount := binary.LittleEndian.Uint64(instruction.Data[8:16]) tokenAmount := binary.LittleEndian.Uint64(instruction.Data[16:24]) return &TxSignal{ TxHash: tx.Signatures[0].String(), Label: "term", Maker: user.String(), Token0Address: mint.String(), Token1Address: wsolMint, Token0Amount: formatTokenAmount(tokenAmount), Token1Amount: formatSolAmount(solAmount), Program: "Pump", Event: "buy", IsToken2022: false, IsMayhemMode: false, ExactSOL: true, Block: tx.Block, Token0AmountUint64: tokenAmount, Token1AmountUint64: solAmount, }, nil } func parseTermBuy(tx *versionedTransaction, instruction *compiledInstruction) (*TxSignal, error) { if len(instruction.Accounts) < 8 { return nil, fmt.Errorf("accounts too short") } staticKeys := tx.Message.StaticAccountKeys mint, err := getStaticKey(staticKeys, int(instruction.Accounts[2])) if err != nil { return nil, err } user, err := getStaticKey(staticKeys, int(instruction.Accounts[6])) if err != nil { return nil, err } solAmount := binary.LittleEndian.Uint64(instruction.Data[8:16]) tokenAmount := binary.LittleEndian.Uint64(instruction.Data[16:24]) return &TxSignal{ TxHash: tx.Signatures[0].String(), Label: "term", Maker: user.String(), Token0Address: mint.String(), Token1Address: wsolMint, Token0Amount: formatTokenAmount(tokenAmount), Token1Amount: formatSolAmount(solAmount), Program: "Pump", Event: "buy", IsToken2022: false, IsMayhemMode: false, ExactSOL: true, Block: tx.Block, Token0AmountUint64: tokenAmount, Token1AmountUint64: solAmount, }, nil } func parseTermSell(tx *versionedTransaction, instruction *compiledInstruction) (*TxSignal, error) { if len(instruction.Accounts) < 8 { return nil, fmt.Errorf("accounts too short") } staticKeys := tx.Message.StaticAccountKeys mint, err := getStaticKey(staticKeys, int(instruction.Accounts[2])) if err != nil { return nil, err } user, err := getStaticKey(staticKeys, int(instruction.Accounts[6])) if err != nil { return nil, err } tokenAmount := binary.LittleEndian.Uint64(instruction.Data[8:16]) solAmount := binary.LittleEndian.Uint64(instruction.Data[16:24]) return &TxSignal{ TxHash: tx.Signatures[0].String(), Label: "term", Maker: user.String(), Token0Address: mint.String(), Token1Address: wsolMint, Token0Amount: formatTokenAmount(tokenAmount), Token1Amount: formatSolAmount(solAmount), Program: "Pump", Event: "buy", IsToken2022: false, IsMayhemMode: false, ExactSOL: false, Block: tx.Block, Token0AmountUint64: tokenAmount, Token1AmountUint64: solAmount, }, nil } func dlmmTokenOrder(tokenX, tokenY solana.PublicKey) (solana.PublicKey, solana.PublicKey) { switch { case tokenX.Equals(solana.WrappedSol): return tokenY, tokenX case tokenY.Equals(solana.WrappedSol): return tokenX, tokenY default: return tokenX, tokenY } } func findAssociatedTokenAddressWithTokenProgram(wallet, mint, tokenProgram solana.PublicKey) (solana.PublicKey, uint8, error) { return solana.FindProgramAddress([][]byte{ wallet[:], tokenProgram[:], mint[:], }, solana.SPLAssociatedTokenAccountProgramID) } type dlmmParsedArgs struct { AmountIn uint64 AmountOut uint64 ExactIn bool ExactOut bool ActiveBin int32 MaxPriceImpactBps uint16 } func parseDlmmSwapArgs(disc []byte, payload []byte) (*dlmmParsedArgs, error) { switch { case bytes.Equal(disc, dlmmSwapIX), bytes.Equal(disc, dlmmSwap2IX): if len(payload) < 16 { return nil, fmt.Errorf("data too short for dlmm swap args, len=%d", len(payload)) } return &dlmmParsedArgs{ AmountIn: binary.LittleEndian.Uint64(payload[0:8]), AmountOut: binary.LittleEndian.Uint64(payload[8:16]), ExactIn: true, }, nil case bytes.Equal(disc, dlmmSwapExactOutIX), bytes.Equal(disc, dlmmSwapExactOut2IX): if len(payload) < 16 { return nil, fmt.Errorf("data too short for dlmm swap exact out args, len=%d", len(payload)) } return &dlmmParsedArgs{ AmountIn: binary.LittleEndian.Uint64(payload[0:8]), AmountOut: binary.LittleEndian.Uint64(payload[8:16]), ExactOut: true, }, nil case bytes.Equal(disc, dlmmSwapPriceImpactIX), bytes.Equal(disc, dlmmSwapPriceImpact2IX): if len(payload) < 11 { return nil, fmt.Errorf("data too short for dlmm swap with price impact args, len=%d", len(payload)) } amountIn := binary.LittleEndian.Uint64(payload[0:8]) idx := 8 if len(payload) < idx+1 { return nil, fmt.Errorf("data too short for dlmm swap with price impact args, len=%d", len(payload)) } activeBinTag := payload[idx] idx++ var activeBin int32 if activeBinTag == 1 { if len(payload) < idx+4 { return nil, fmt.Errorf("data too short for dlmm swap with price impact args, len=%d", len(payload)) } activeBin = int32(binary.LittleEndian.Uint32(payload[idx : idx+4])) idx += 4 } else if activeBinTag != 0 { return nil, fmt.Errorf("invalid active_id tag %d", activeBinTag) } if len(payload) < idx+2 { return nil, fmt.Errorf("data too short for dlmm swap with price impact args, len=%d", len(payload)) } return &dlmmParsedArgs{ AmountIn: amountIn, ExactIn: true, ActiveBin: activeBin, MaxPriceImpactBps: binary.LittleEndian.Uint16(payload[idx : idx+2]), }, nil default: return nil, nil } } func parseDlmmInstruction(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) { msg := tx.Message if instructionIndex >= len(msg.Instructions) { return nil, fmt.Errorf("instruction index out of bounds") } instruction := msg.Instructions[instructionIndex] if len(instruction.Data) < 8 { return nil, fmt.Errorf("data is empty") } if len(instruction.Accounts) < 13 { return nil, fmt.Errorf("accounts too short") } disc := instruction.Data[:8] payload := instruction.Data[8:] args, err := parseDlmmSwapArgs(disc, payload) if err != nil { return nil, err } if args == nil { return nil, nil } staticKeys := tx.Message.StaticAccountKeys userTokenIn, err := getStaticKey(staticKeys, int(instruction.Accounts[4])) if err != nil { return nil, err } userTokenOut, err := getStaticKey(staticKeys, int(instruction.Accounts[5])) if err != nil { return nil, err } tokenX, err := getStaticKey(staticKeys, int(instruction.Accounts[6])) if err != nil { return nil, err } tokenY, err := getStaticKey(staticKeys, int(instruction.Accounts[7])) if err != nil { return nil, err } user, err := getStaticKey(staticKeys, int(instruction.Accounts[10])) if err != nil { return nil, err } tokenXProgram, err := getStaticKey(staticKeys, int(instruction.Accounts[11])) if err != nil { return nil, err } tokenYProgram, err := getStaticKey(staticKeys, int(instruction.Accounts[12])) if err != nil { return nil, err } token0Mint, token1Mint := dlmmTokenOrder(tokenX, tokenY) var ( token0AmountUint64 uint64 token1AmountUint64 uint64 ) if !tokenX.Equals(solana.WrappedSol) && !tokenY.Equals(solana.WrappedSol) { return nil, nil } wsolProgram := tokenXProgram if tokenY.Equals(solana.WrappedSol) { wsolProgram = tokenYProgram } wsolAta, _, err := findAssociatedTokenAddressWithTokenProgram(user, solana.WrappedSol, wsolProgram) if err != nil { return nil, nil } wsolIn := userTokenIn.Equals(wsolAta) wsolOut := userTokenOut.Equals(wsolAta) if !wsolIn && !wsolOut { return nil, nil } event := "sell" if wsolIn { event = "buy" } exactSol := (args.ExactIn && wsolIn) || (args.ExactOut && wsolOut) if wsolIn { if args.ExactIn { token1AmountUint64 = args.AmountIn } if args.ExactOut { token0AmountUint64 = args.AmountOut } } else { if args.ExactOut { token1AmountUint64 = args.AmountOut } if args.ExactIn { token0AmountUint64 = args.AmountIn } } token0Amount := formatTokenAmount(token0AmountUint64) if token0Mint.Equals(solana.WrappedSol) { token0Amount = formatSolAmount(token0AmountUint64) } token1Amount := decimal.Zero if token1AmountUint64 > 0 { if token1Mint.Equals(solana.WrappedSol) { token1Amount = formatSolAmount(token1AmountUint64) } else { token1Amount = formatTokenAmount(token1AmountUint64) } } return &TxSignal{ TxHash: tx.Signatures[0].String(), Label: "dlmm", Maker: user.String(), Token0Address: token0Mint.String(), Token1Address: token1Mint.String(), Token0Amount: token0Amount, Token1Amount: token1Amount, Program: "MeteoraDLMM", Event: event, IsToken2022: false, IsMayhemMode: false, ExactSOL: exactSol, ActiveBin: args.ActiveBin, MaxPriceImpactBps: args.MaxPriceImpactBps, Block: tx.Block, Token0AmountUint64: token0AmountUint64, Token1AmountUint64: token1AmountUint64, }, nil } func decodePumpAmmBuyArgs(data []byte) (uint64, uint64, error) { if len(data) < 9 { return 0, 0, fmt.Errorf("data too short for pump amm buy args, len=%d", len(data)) } var args pumpAmmBuyArgs if err := borsh.Deserialize(&args, data[8:]); err == nil { return args.Amount, args.MaxSolCost, nil } if len(data) >= 24 { amount := binary.LittleEndian.Uint64(data[8:16]) maxSol := binary.LittleEndian.Uint64(data[16:24]) return amount, maxSol, nil } return 0, 0, fmt.Errorf("failed to parse buy tokens args") } func parsePumpAmmBuy(tx *versionedTransaction, instruction *compiledInstruction) (*TxSignal, error) { amount, maxSol, err := decodePumpAmmBuyArgs(instruction.Data) if err != nil { return nil, err } exactIn := false if matchMethod(instruction.Data, pumpAmmBuyTokensV2IX) { temp := amount amount = maxSol maxSol = temp exactIn = true } if len(instruction.Accounts) < 7 { return nil, fmt.Errorf("accounts too short") } staticKeys := tx.Message.StaticAccountKeys base, err := getStaticKey(staticKeys, int(instruction.Accounts[3])) if err != nil { return nil, err } quote, err := getStaticKey(staticKeys, int(instruction.Accounts[4])) if err != nil { return nil, err } if !quote.Equals(solana.WrappedSol) { return nil, nil } buyer, err := getStaticKey(staticKeys, int(instruction.Accounts[1])) if err != nil { return nil, err } return &TxSignal{ TxHash: tx.Signatures[0].String(), Label: "pumpamm", Maker: buyer.String(), Token0Address: base.String(), Token1Address: wsolMint, Token0Amount: formatTokenAmount(amount), Token1Amount: formatSolAmount(maxSol), Program: "PumpAMM", Event: "buy", IsToken2022: false, IsMayhemMode: false, ExactSOL: exactIn, Block: tx.Block, Token0AmountUint64: amount, Token1AmountUint64: maxSol, }, nil } func parsePumpAmmSell(tx *versionedTransaction, instruction *compiledInstruction) (*TxSignal, error) { amount, minSol, err := decodePumpAmmBuyArgs(instruction.Data) if err != nil { return nil, err } if len(instruction.Accounts) < 7 { return nil, fmt.Errorf("accounts too short") } staticKeys := tx.Message.StaticAccountKeys base, err := getStaticKey(staticKeys, int(instruction.Accounts[3])) if err != nil { return nil, err } quote, err := getStaticKey(staticKeys, int(instruction.Accounts[4])) if err != nil { return nil, err } if !quote.Equals(solana.WrappedSol) { return nil, nil } buyer, err := getStaticKey(staticKeys, int(instruction.Accounts[1])) if err != nil { return nil, err } return &TxSignal{ TxHash: tx.Signatures[0].String(), Label: "pumpamm", Maker: buyer.String(), Token0Address: base.String(), Token1Address: wsolMint, Token0Amount: formatTokenAmount(amount), Token1Amount: formatSolAmount(minSol), Program: "PumpAMM", Event: "sell", IsToken2022: false, IsMayhemMode: false, Block: tx.Block, Token0AmountUint64: amount, Token1AmountUint64: minSol, }, nil } func parseBoboInstruction(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) { msg := tx.Message if instructionIndex >= len(msg.Instructions) { return nil, fmt.Errorf("instruction index out of bounds") } instruction := msg.Instructions[instructionIndex] if len(instruction.Data) == 0 { return nil, fmt.Errorf("data is empty") } if len(instruction.Data) < 8 || !bytes.Equal(instruction.Data[:8], boboBuyPumpTokensIX) { return nil, nil } if len(instruction.Accounts) < 8 { return nil, fmt.Errorf("accounts too short") } if len(instruction.Data) < 16 { return nil, fmt.Errorf("data too short for bobo buy args, len=%d", len(instruction.Data)) } staticKeys := tx.Message.StaticAccountKeys mint, err := getStaticKey(staticKeys, int(instruction.Accounts[2])) if err != nil { return nil, err } user, err := getStaticKey(staticKeys, int(instruction.Accounts[6])) if err != nil { return nil, err } var args boboBuyArgs if err := borsh.Deserialize(&args, instruction.Data[8:]); err != nil { return nil, fmt.Errorf("failed to parse buy tokens args: %w", err) } return &TxSignal{ TxHash: tx.Signatures[0].String(), Label: "bobo", Maker: user.String(), Token0Address: mint.String(), Token1Address: wsolMint, Token0Amount: decimal.NewFromInt(1), Token1Amount: formatSolAmount(args.SolAmount), Program: "Pump", Event: "buy", IsToken2022: false, IsMayhemMode: false, Block: tx.Block, Token0AmountUint64: 1, Token1AmountUint64: args.SolAmount, }, nil } func parseQtkvInstruction(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) { msg := tx.Message if instructionIndex >= len(msg.Instructions) { return nil, fmt.Errorf("instruction index out of bounds") } instruction := msg.Instructions[instructionIndex] if len(instruction.Data) == 0 { return nil, fmt.Errorf("data is empty") } if matchMethod(instruction.Data, qtkvBuyTokensIX) { return parseQtkvBuy(tx, instructionIndex) } else if matchMethod(instruction.Data, qtkvAmmSellTokensIX) { return parseQtkvAmmSell(tx, instructionIndex) } else if matchMethod(instruction.Data, qtkvSellTokensIX) { return parseQtkvSell(tx, instructionIndex) } return nil, nil } func parseQtkvSell(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) { instruction := tx.Message.Instructions[instructionIndex] if len(instruction.Accounts) < 11 { return nil, fmt.Errorf("accounts too short") } if len(instruction.Data) < 24 { return nil, fmt.Errorf("data too short for qtkv sell args, len=%d", len(instruction.Data)) } staticKeys := tx.Message.StaticAccountKeys mint, err := getStaticKey(staticKeys, int(instruction.Accounts[10])) if err != nil { return nil, err } user, err := getStaticKey(staticKeys, int(instruction.Accounts[0])) if err != nil { return nil, err } // in sell, sol amount is not directly provided, so we set it to 0 tokenAmount := binary.LittleEndian.Uint64(instruction.Data[19:25]) return &TxSignal{ TxHash: tx.Signatures[0].String(), Label: "qtkv", Maker: user.String(), Token0Address: mint.String(), Token1Address: wsolMint, Token0Amount: formatTokenAmount(tokenAmount), Token1Amount: decimal.Zero, Program: "Pump", Event: "sell", IsToken2022: false, IsMayhemMode: false, Block: tx.Block, Token0AmountUint64: tokenAmount, Token1AmountUint64: 0, }, nil } func parseQtkvAmmSell(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) { instruction := tx.Message.Instructions[instructionIndex] if len(instruction.Accounts) < 11 { return nil, fmt.Errorf("accounts too short") } if len(instruction.Data) < 24 { return nil, fmt.Errorf("data too short for qtkv amm sell args, len=%d", len(instruction.Data)) } staticKeys := tx.Message.StaticAccountKeys mint, err := getStaticKey(staticKeys, int(instruction.Accounts[10])) if err != nil { return nil, err } user, err := getStaticKey(staticKeys, int(instruction.Accounts[0])) if err != nil { return nil, err } // in sell, sol amount is not directly provided, so we set it to 0 tokenAmount := binary.LittleEndian.Uint64(instruction.Data[19:25]) return &TxSignal{ TxHash: tx.Signatures[0].String(), Label: "qtkv", Maker: user.String(), Token0Address: mint.String(), Token1Address: wsolMint, Token0Amount: formatTokenAmount(tokenAmount), Token1Amount: decimal.Zero, Program: "PumpAMM", Event: "sell", IsToken2022: false, IsMayhemMode: false, Block: tx.Block, Token0AmountUint64: tokenAmount, Token1AmountUint64: 0, }, nil } func parseQtkvBuy(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) { instruction := tx.Message.Instructions[instructionIndex] if len(instruction.Accounts) < 8 { return nil, fmt.Errorf("accounts too short") } staticKeys := tx.Message.StaticAccountKeys mint, err := getStaticKey(staticKeys, int(instruction.Accounts[3])) if err != nil { return nil, err } user, err := getStaticKey(staticKeys, int(instruction.Accounts[0])) if err != nil { return nil, err } var args qtkvBuyArgs if err := borsh.Deserialize(&args, instruction.Data[1:]); err != nil { return nil, fmt.Errorf("failed to parse buy tokens args: %w", err) } return &TxSignal{ TxHash: tx.Signatures[0].String(), Label: "qtkv", Maker: user.String(), Token0Address: mint.String(), Token1Address: wsolMint, Token0Amount: formatTokenAmount(args.TokenNumber), Token1Amount: formatSolAmount(args.SolAmount), Program: "Pump", Event: "buy", IsToken2022: false, IsMayhemMode: false, Block: tx.Block, Token0AmountUint64: args.TokenNumber, Token1AmountUint64: args.SolAmount, }, nil } func parseFjszInstruction(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) { msg := tx.Message if instructionIndex >= len(msg.Instructions) { return nil, fmt.Errorf("instruction index out of bounds") } instruction := msg.Instructions[instructionIndex] if len(instruction.Data) == 0 { return nil, fmt.Errorf("data is empty") } if !matchMethod(instruction.Data, fjszBuyTokensIX) { return nil, nil } if len(instruction.Accounts) < 7 { return nil, fmt.Errorf("accounts too short") } if len(instruction.Data) < 16 { return nil, fmt.Errorf("data too short for fjzs buy args, len=%d", len(instruction.Data)) } staticKeys := tx.Message.StaticAccountKeys mint, err := getStaticKey(staticKeys, int(instruction.Accounts[2])) if err != nil { return nil, err } user, err := getStaticKey(staticKeys, int(instruction.Accounts[6])) if err != nil { return nil, err } var args fjszBuyArgs if err := borsh.Deserialize(&args, instruction.Data[8:]); err != nil { return nil, fmt.Errorf("failed to parse buy tokens args: %w", err) } return &TxSignal{ TxHash: tx.Signatures[0].String(), Label: "fjsz", Maker: user.String(), Token0Address: mint.String(), Token1Address: wsolMint, Token0Amount: formatTokenAmount(args.TokenAmount), Token1Amount: formatSolAmount(args.SolAmount), Program: "Pump", Event: "buy", IsToken2022: false, IsMayhemMode: false, Block: tx.Block, Token0AmountUint64: args.TokenAmount, Token1AmountUint64: args.SolAmount, }, nil } func parseBonkInstruction(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) { msg := tx.Message if instructionIndex >= len(msg.Instructions) { return nil, fmt.Errorf("instruction index out of bounds") } instruction := msg.Instructions[instructionIndex] if len(instruction.Data) == 0 { return nil, fmt.Errorf("data is empty") } if matchMethod(instruction.Data, bonkBuyAndSellTokensIX) { return parseBonkBuyAndSell(tx, &instruction) } return nil, nil } func parseBonkBuyAndSell(tx *versionedTransaction, instruction *compiledInstruction) (*TxSignal, error) { if len(instruction.Accounts) < 8 { return nil, fmt.Errorf("accounts too short") } staticKeys := tx.Message.StaticAccountKeys programId, err := getStaticKey(staticKeys, int(instruction.Accounts[7])) if err != nil { return nil, err } if programId != pumpProgramID { return nil, nil } user, err := getStaticKey(staticKeys, int(instruction.Accounts[0])) if err != nil { return nil, err } flagAccount, err := getStaticKey(staticKeys, int(instruction.Accounts[4])) if err != nil { return nil, err } amount1 := binary.LittleEndian.Uint64(instruction.Data[17:25]) amount2 := binary.LittleEndian.Uint64(instruction.Data[25:33]) if user == flagAccount { mint, err := getStaticKey(staticKeys, int(instruction.Accounts[6])) if err != nil { return nil, err } return &TxSignal{ TxHash: tx.Signatures[0].String(), Label: "bonk", Maker: user.String(), Token0Address: mint.String(), Token1Address: wsolMint, Token0Amount: formatTokenAmount(amount2), Token1Amount: formatSolAmount(amount1), Program: "Pump", Event: "buy", IsToken2022: false, IsMayhemMode: false, ExactSOL: true, Block: tx.Block, Token0AmountUint64: amount2, Token1AmountUint64: amount1, }, nil } else { mint, err := getStaticKey(staticKeys, int(instruction.Accounts[5])) if err != nil { return nil, err } return &TxSignal{ TxHash: tx.Signatures[0].String(), Label: "bonk", Maker: user.String(), Token0Address: mint.String(), Token1Address: wsolMint, Token0Amount: formatTokenAmount(amount1), Token1Amount: formatSolAmount(amount2), Program: "Pump", Event: "sell", IsToken2022: false, IsMayhemMode: false, ExactSOL: false, Block: tx.Block, Token0AmountUint64: amount1, Token1AmountUint64: amount2, }, nil } } func parseBloomRouterInstruction(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) { msg := tx.Message if instructionIndex >= len(msg.Instructions) { return nil, fmt.Errorf("instruction index out of bounds") } instruction := msg.Instructions[instructionIndex] if len(instruction.Data) < 26 { return nil, nil } var ( amount uint64 sol uint64 exactIn bool event string ) args, err := decodeBloomRouterArgs(instruction.Data) if err != nil { return nil, err } switch args.Side { case 0: event = "buy" exactIn = true case 1: event = "sell" default: return nil, nil } if args.SolAmount > ^uint64(0)/100 { return nil, fmt.Errorf("bloomrouter sol amount overflow") } // bloomrouter SOL amount has 2 fewer decimals than lamports. sol = args.SolAmount * 100 amount = args.TokenAmount if len(instruction.Accounts) == 0 { return nil, fmt.Errorf("accounts too short") } maker, err := getStaticKey(msg.StaticAccountKeys, int(instruction.Accounts[0])) if err != nil { return nil, err } var ( mint solana.PublicKey ok bool ) for _, acctIdx := range instruction.Accounts { key, err := getStaticKey(msg.StaticAccountKeys, int(acctIdx)) if err != nil { return nil, err } if strings.HasSuffix(key.String(), "pump") { mint = key ok = true break } } if !ok { return nil, nil } return &TxSignal{ TxHash: tx.Signatures[0].String(), Label: "bloomrouter", Maker: maker.String(), Token0Address: mint.String(), Token1Address: wsolMint, Token0Amount: formatTokenAmount(amount), Token1Amount: formatSolAmount(sol), Program: "Pump", Event: event, ExactSOL: exactIn, IsToken2022: false, IsMayhemMode: false, Block: tx.Block, Token0AmountUint64: amount, Token1AmountUint64: sol, }, nil } func decodeBloomRouterArgs(data []byte) (bloomRouterArgs, error) { if len(data) < 26 { return bloomRouterArgs{}, fmt.Errorf("data too short for bloomrouter args, len=%d", len(data)) } return bloomRouterArgs{ Side: binary.BigEndian.Uint16(data[8:10]), SolAmount: binary.LittleEndian.Uint64(data[10:18]), TokenAmount: binary.LittleEndian.Uint64(data[18:26]), }, nil } func matchMethod(data []byte, methods []byte) bool { if len(data) < len(methods) { return false } return bytes.Equal(data[0:len(methods)], methods) }