package txparser import ( "bytes" "encoding/binary" "fmt" "log" "math/big" "github.com/gagliardetto/solana-go" "github.com/near/borsh-go" "github.com/samlior/libsam/pkg/types" "github.com/samlior/libsam/third_party/shreder_protos" "github.com/shopspring/decimal" ) const ( wsolMint = "So11111111111111111111111111111111111111112" tokenProgram = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" ) // program ids const ( pumpProgramID = "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P" azczProgramID = "AzcZqCRUQgKEg5FTAgY7JacATABEYCEfMbjXEzspLYFB" f5tfProgramID = "F5tfvbLog9VdGUPqBDTT8rgXvTTcq7e5UiGnupL1zvBq" photonProgramID = "BSfD6SHZigAfDWSjzD5Q41jw8LmKwtmjskPH9XW1mrRW" pumpAmmProgramID = "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA" _6hb1ProgramID = "6HB1VBBS8LrdQiR9MZcXV5VdpKFb7vjTMZuQQEQEPioC" _8rsrProgramID = "8rsrfqvEQFKDm66uGx1swVJhkGf2fDet5Q7bygUjYYA3" boboProgramID = "BobogA5N2KN2GG4XN3E3rNNRw3L8H1QPXp7QLxGrNHGM" qtkvProgramID = "qtkvapJEvRWWrB7i5K6RaA1kvq5x3qmMKZ98ad71XQ7" fjszProgramID = "FJsZbftBqRLfF7uqUKpm4s2goDr6xsQ5Q3mN7AFJB6hK" flasProgramID = "FLASHX8DrLbgeR8FcfNV1F5krxYcYMUdBkrP1EPBtxB9" ) // instruction discriminators const ( pumpCreateCoinIX = uint8(24) pumpCreateCoinV2IX = uint8(214) pumpExtendedSellIX = uint8(51) pumpBuyTokensIX = uint8(102) azczBuyTokensIX = uint8(11) f5tfBuyTokensIX = uint8(0) flasBuyTokensIX = uint8(0) pumpAmmBuyTokensIX = uint8(102) _6hb1BuyTokensIX = uint8(0) qtkvBuyTokensIX = uint8(0x02) ) var ( boboBuyPumpTokensIX = []byte{0xff, 0xe7, 0x11, 0x53, 0x15, 0xc5, 0xc3, 0xdf} fjszBuyTokensIX = []byte{0xe7, 0x3f, 0x99, 0x83, 0xf3, 0xed, 0xe3, 0x3c} _8rsrBuyTokensIX = []byte{0xd9, 0xa1, 0x42, 0x0c, 0xbb, 0xc4, 0xe8, 0x30} photonBuyPumpTokensIX = []byte{0x52, 0xe1, 0x77, 0xe7, 0x4e, 0x1d, 0x2d, 0x46} photonSwapPumpAmmIX = []byte{0x2c, 0x77, 0xaf, 0xda, 0xc7, 0x4d, 0xc4, 0xeb} ) // table lookups const ( _6hb1TableLookup1 = "GwME2SEvkgtiL5d2UeR2ZCsEecvg879majGF5pAA1aTS" _6hb1TableLookup2 = "7RKtfATWCe98ChuwecNq8XCzAzfoK3DtZTprFsPMGtio" _8rsrTableLookup1 = "DSaHkhDp17UexbZsg2VUnWjEuTwKNCJrnG4LW122ANfd" photonTableLookup = "3r6paeFSLpeUVmWtShb5uZtXYpcBE3729kUxkUS7xKi1" ) 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 } 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 flasBuyArgs struct { SolAmount uint64 TokenAmount uint64 Placeholder [3]uint8 } type photonBuyPumpArgs struct { Timestamp uint64 SolAmount uint64 TokenAmount uint64 Fee uint64 } type photonSwapPumpAmmArgs struct { FromAmount uint64 ToAmount uint64 } type pumpAmmBuyArgs struct { Amount uint64 MaxSolCost uint64 } type _6hb1BuyArgs struct { SolAmount uint64 TokenNumber uint64 } type _8rsrBuyArgs struct { SolIn uint64 TokenOut 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 } // ParseTransaction mirrors the Rust parse_transaction entry point. func ParseTransaction(update *shreder_protos.SubscribeUpdateTransaction) []*types.TxSignal { versioned, err := toVersionedTransaction(update) if err != nil || versioned == nil || len(versioned.Signatures) == 0 { return nil } txHash := versioned.Signatures[0].String() staticKeys := versioned.Message.StaticAccountKeys instructions := versioned.Message.Instructions var parsed []*types.TxSignal for i := range instructions { inst := instructions[i] if int(inst.ProgramIDIndex) >= len(staticKeys) { continue } programID := staticKeys[inst.ProgramIDIndex].String() switch programID { case pumpProgramID: txRes, err := parsePumpInstruction(versioned, i) parsed = appendParsed(parsed, txRes, err, txHash, "pump") case azczProgramID: txRes, err := parseAzczInstruction(versioned, i) parsed = appendParsed(parsed, txRes, err, txHash, "azcz") case f5tfProgramID: txRes, err := parseF5tfInstruction(versioned, i) parsed = appendParsed(parsed, txRes, err, txHash, "f5tf") case flasProgramID: txRes, err := parseFlasInstruction(versioned, i) parsed = appendParsed(parsed, txRes, err, txHash, "flas") case photonProgramID: txRes, err := parsePhotonInstruction(versioned, i) parsed = appendParsed(parsed, txRes, err, txHash, "photon") case pumpAmmProgramID: txRes, err := parsePumpAmmInstruction(versioned, i) parsed = appendParsed(parsed, txRes, err, txHash, "pumpamm") case _6hb1ProgramID: txRes, err := parse6hb1Instruction(versioned, i) parsed = appendParsed(parsed, txRes, err, txHash, "6hb1") case _8rsrProgramID: txRes, err := parse8rsrInstruction(versioned, i) parsed = appendParsed(parsed, txRes, err, txHash, "8rsr") case boboProgramID: txRes, err := parseBoboInstruction(versioned, i) parsed = appendParsed(parsed, txRes, err, txHash, "bobo") case qtkvProgramID: txRes, err := parseQtkvInstruction(versioned, i) parsed = appendParsed(parsed, txRes, err, txHash, "qtkv") case fjszProgramID: txRes, err := parseFjszInstruction(versioned, i) parsed = appendParsed(parsed, txRes, err, txHash, "fjsz") } } return parsed } func appendParsed(list []*types.TxSignal, parsed *types.TxSignal, err error, txHash string, label string) []*types.TxSignal { if err != nil { log.Printf("txparser: failed to parse %s instruction: %v, tx_hash: %s", label, err, txHash) return list } if parsed != nil { list = append(list, parsed) } return list } func toVersionedTransaction(update *shreder_protos.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 signatures := make([]solana.Signature, len(protoTx.Signatures)) for i, rawSig := range protoTx.Signatures { signatures[i] = solana.SignatureFromBytes(rawSig) } staticKeys := make([]solana.PublicKey, len(msg.AccountKeys)) for i, key := range msg.AccountKeys { staticKeys[i] = solana.PublicKeyFromBytes(key) } instructions := make([]compiledInstruction, len(msg.Instructions)) for i, instr := range msg.Instructions { accounts := append([]uint8(nil), instr.Accounts...) instructions[i] = compiledInstruction{ ProgramIDIndex: uint8(instr.ProgramIdIndex), Accounts: accounts, Data: instr.Data, } } lookups := make([]addressTableLookup, len(msg.AddressTableLookups)) for i, lookup := range msg.AddressTableLookups { writable := append([]uint8(nil), lookup.WritableIndexes...) readonly := append([]uint8(nil), lookup.ReadonlyIndexes...) lookups[i] = addressTableLookup{ AccountKey: solana.PublicKeyFromBytes(lookup.AccountKey), WritableIndexes: writable, ReadonlyIndexes: readonly, } } return &versionedTransaction{ Signatures: signatures, Message: versionedMessage{ StaticAccountKeys: staticKeys, Instructions: instructions, AddressTableLookups: lookups, }, Block: update.GetSlot(), }, 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{}, fmt.Errorf("account index %d out of bounds", index) } return static[index], nil } func parsePumpInstruction(tx *versionedTransaction, instructionIndex int) (*types.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") } switch instruction.Data[0] { case pumpBuyTokensIX: return parsePumpBuy(tx, &instruction) case pumpExtendedSellIX: return parsePumpSell(tx, &instruction) case pumpCreateCoinIX: return parsePumpCreate(tx, &instruction) case pumpCreateCoinV2IX: return parsePumpCreateV2(tx, &instruction) default: return nil, nil } } func parsePumpCreate(tx *versionedTransaction, instruction *compiledInstruction) (*types.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 &types.TxSignal{ TxHash: tx.Signatures[0].String(), 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) (*types.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 create v2 args") } 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 &types.TxSignal{ TxHash: tx.Signatures[0].String(), 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 buy args") } 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) (*types.TxSignal, error) { amount, maxSol, err := decodePumpBuyArgs(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 } buyer, err := getStaticKey(staticKeys, int(instruction.Accounts[6])) if err != nil { return nil, err } return &types.TxSignal{ TxHash: tx.Signatures[0].String(), Maker: buyer.String(), Token0Address: mint.String(), Token1Address: wsolMint, Token0Amount: formatTokenAmount(amount), Token1Amount: formatSolAmount(maxSol), Program: "Pump", Event: "buy", IsToken2022: false, IsMayhemMode: false, Block: tx.Block, Token0AmountUint64: amount, Token1AmountUint64: maxSol, }, nil } func decodePumpSellArgs(data []byte) (uint64, uint64, error) { if len(data) < 9 { return 0, 0, fmt.Errorf("data too short for sell args") } 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) (*types.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 &types.TxSignal{ TxHash: tx.Signatures[0].String(), 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) (*types.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 instruction.Data[0] != azczBuyTokensIX { return nil, nil } if len(instruction.Accounts) < 8 { return nil, fmt.Errorf("accounts too short") } staticKeys := msg.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 buy args") } 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 &types.TxSignal{ TxHash: tx.Signatures[0].String(), 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) (*types.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 instruction.Data[0] != 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 buy args") } 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 &types.TxSignal{ TxHash: tx.Signatures[0].String(), 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) (*types.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 instruction.Data[0] != flasBuyTokensIX { return nil, nil } if len(instruction.Accounts) < 9 { return nil, fmt.Errorf("accounts too short") } staticKeys := msg.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) < 2 { return nil, fmt.Errorf("data too short for buy args") } var args flasBuyArgs if err := borsh.Deserialize(&args, instruction.Data[1:]); err != nil { return nil, fmt.Errorf("failed to parse buy tokens args: %w", err) } return &types.TxSignal{ TxHash: tx.Signatures[0].String(), 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 parsePhotonInstruction(tx *versionedTransaction, instructionIndex int) (*types.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) (*types.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 buy args") } 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 &types.TxSignal{ TxHash: tx.Signatures[0].String(), Maker: user.String(), Token0Address: mint.String(), Token1Address: wsolMint, Token0Amount: formatTokenAmount(args.TokenAmount), Token1Amount: formatSolAmount(solAmount), Program: "Pump", Event: "buy", IsToken2022: false, IsMayhemMode: false, Block: tx.Block, Token0AmountUint64: args.TokenAmount, Token1AmountUint64: solAmount, }, nil } func parsePhotonSwap(tx *versionedTransaction, instruction *compiledInstruction) (*types.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") } staticKeys := tx.Message.StaticAccountKeys base, err := getStaticKey(staticKeys, int(instruction.Accounts[3])) if err != nil { return nil, err } quoteIndex := int(instruction.Accounts[4]) quote, err := resolveQuoteAccount(tx, quoteIndex, []string{photonTableLookup}, 0) if err != nil { return nil, err } if quote != wsolMint { 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 &types.TxSignal{ TxHash: tx.Signatures[0].String(), 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) (*types.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 instruction.Data[0] != pumpAmmBuyTokensIX { return nil, nil } return parsePumpAmmBuy(tx, &instruction) } func decodePumpAmmBuyArgs(data []byte) (uint64, uint64, error) { if len(data) < 9 { return 0, 0, fmt.Errorf("data too short for buy args") } 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) (*types.TxSignal, error) { amount, maxSol, 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 } quoteIndex := int(instruction.Accounts[4]) quote, err := resolveQuoteAccount(tx, quoteIndex, nil, 0) if err != nil { return nil, err } if quote != wsolMint { return nil, nil } buyer, err := getStaticKey(staticKeys, int(instruction.Accounts[1])) if err != nil { return nil, err } return &types.TxSignal{ TxHash: tx.Signatures[0].String(), Maker: buyer.String(), Token0Address: base.String(), Token1Address: wsolMint, Token0Amount: formatTokenAmount(amount), Token1Amount: formatSolAmount(maxSol), Program: "PumpAMM", Event: "buy", IsToken2022: false, IsMayhemMode: false, Block: tx.Block, Token0AmountUint64: amount, Token1AmountUint64: maxSol, }, nil } func parse6hb1Instruction(tx *versionedTransaction, instructionIndex int) (*types.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 instruction.Data[0] != _6hb1BuyTokensIX { return nil, nil } return parse6hb1Buy(tx, &instruction) } func parse6hb1Buy(tx *versionedTransaction, instruction *compiledInstruction) (*types.TxSignal, error) { 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 } quoteIndex := int(instruction.Accounts[4]) quote, err := resolveQuoteAccount(tx, quoteIndex, []string{_6hb1TableLookup1, _6hb1TableLookup2}, 7) if err != nil { return nil, err } if quote != wsolMint { return nil, nil } buyer, err := getStaticKey(staticKeys, int(instruction.Accounts[1])) if err != nil { return nil, err } if len(instruction.Data) < 2 { return nil, fmt.Errorf("data too short for buy args") } var args _6hb1BuyArgs if err := borsh.Deserialize(&args, instruction.Data[1:]); err != nil { return nil, fmt.Errorf("failed to parse buy tokens args: %w", err) } return &types.TxSignal{ TxHash: tx.Signatures[0].String(), Maker: buyer.String(), Token0Address: base.String(), Token1Address: wsolMint, Token0Amount: formatTokenAmount(args.TokenNumber), Token1Amount: formatSolAmount(args.SolAmount), Program: "PumpAMM", Event: "buy", IsToken2022: false, IsMayhemMode: false, Block: tx.Block, Token0AmountUint64: args.TokenNumber, Token1AmountUint64: args.SolAmount, }, nil } func parse8rsrInstruction(tx *versionedTransaction, instructionIndex int) (*types.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], _8rsrBuyTokensIX) { return nil, nil } return parse8rsrBuy(tx, &instruction) } func parse8rsrBuy(tx *versionedTransaction, instruction *compiledInstruction) (*types.TxSignal, error) { 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 buy args") } staticKeys := tx.Message.StaticAccountKeys base, err := getStaticKey(staticKeys, int(instruction.Accounts[3])) if err != nil { return nil, err } quoteIndex := int(instruction.Accounts[4]) quote, err := resolveQuoteAccount(tx, quoteIndex, []string{_8rsrTableLookup1}, 1) if err != nil { return nil, err } if quote != wsolMint { return nil, nil } buyer, err := getStaticKey(staticKeys, int(instruction.Accounts[1])) if err != nil { return nil, err } var args _8rsrBuyArgs if err := borsh.Deserialize(&args, instruction.Data[8:]); err != nil { return nil, fmt.Errorf("failed to parse buy tokens args: %w", err) } return &types.TxSignal{ TxHash: tx.Signatures[0].String(), Maker: buyer.String(), Token0Address: base.String(), Token1Address: wsolMint, Token0Amount: formatTokenAmount(args.TokenOut), Token1Amount: formatSolAmount(args.SolIn), Program: "PumpAMM", Event: "buy", IsToken2022: false, IsMayhemMode: false, Block: tx.Block, Token0AmountUint64: args.TokenOut, Token1AmountUint64: args.SolIn, }, nil } func parseBoboInstruction(tx *versionedTransaction, instructionIndex int) (*types.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 buy args") } 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 &types.TxSignal{ TxHash: tx.Signatures[0].String(), 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) (*types.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 instruction.Data[0] != qtkvBuyTokensIX { return nil, nil } if len(instruction.Data) < 2 { return nil, fmt.Errorf("data too short for buy args") } 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 &types.TxSignal{ TxHash: tx.Signatures[0].String(), 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) (*types.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], 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 buy args") } 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 &types.TxSignal{ TxHash: tx.Signatures[0].String(), 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 resolveQuoteAccount(tx *versionedTransaction, quoteIndex int, expectedTableKeys []string, targetIndex uint8) (string, error) { staticKeys := tx.Message.StaticAccountKeys if quoteIndex < len(staticKeys) { quoteKey := staticKeys[quoteIndex].String() return quoteKey, nil } // attempt to load from address table lookup if len(expectedTableKeys) == 0 || len(tx.Message.AddressTableLookups) != 1 { return "", fmt.Errorf("parse quote from table lookup failed") } table := tx.Message.AddressTableLookups[0] match := false for _, key := range expectedTableKeys { if table.AccountKey.String() == key { match = true break } } if !match { return "", fmt.Errorf("parse quote from table lookup failed") } indexOfTarget := indexOf(table.ReadonlyIndexes, targetIndex) if indexOfTarget < 0 { return "", fmt.Errorf("parse quote from table lookup failed") } expectedIndex := len(staticKeys) + len(table.WritableIndexes) + indexOfTarget if quoteIndex != expectedIndex { return "", fmt.Errorf("parse quote from table lookup failed") } return wsolMint, nil } func indexOf(haystack []uint8, needle uint8) int { for i, v := range haystack { if v == needle { return i } } return -1 }