package shreder import ( "bytes" "encoding/binary" "fmt" "math/big" "slices" "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") ) 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} ) type addressTableLookup struct { AccountKeyOffset int WriteOffset int WriteLen int ReadOffset int ReadLen int } type TransactionGetter interface { GetAccount(idx uint8) (solana.PublicKey, error) Signatures() string Block() uint64 GetAddressTableLookups(idx int) (account solana.PublicKey, write []uint8, read []uint8, err error) GetAddressTablesLookupsLen() int GetInstruction(idx int) (program solana.PublicKey, accounts []uint8, data []byte, err error) GetInstructionLen() int GetStaticAccountKeys(idx uint8) (account solana.PublicKey, err error) GetStaticAccountKeysLen() uint8 } type FillableTransaction interface { FillLookupTable(account solana.PublicKey) } func (tx *versionedTransaction) FillLookupTable(account solana.PublicKey) { tx.TableLookups = append(tx.TableLookups, account) } func (tx *versionedTransaction) GetAccount(idx uint8) (solana.PublicKey, error) { idxMax := tx.staticAccountKeys + uint8(len(tx.TableLookups)) if idx < 0 || idx >= idxMax { return solana.PublicKey{}, fmt.Errorf("account index %d out of range, len=%d", idx, idxMax) } if idx < tx.staticAccountKeys { return tx.GetStaticAccountKeys(idx) } idx -= tx.staticAccountKeys if idx < uint8(len(tx.TableLookups)) { return tx.TableLookups[idx], nil } return solana.PublicKey{}, fmt.Errorf("account index %d out of range, len=%d", idx, len(tx.TableLookups)) } func (tx *versionedTransaction) GetInstructionLen() int { return tx.instructions } func (tx *versionedTransaction) GetInstruction(idx int) (program solana.PublicKey, accounts []uint8, data []byte, err error) { if idx < 0 || idx >= tx.instructions { err = fmt.Errorf("instruction index %d out of range, len=%d", idx, tx.instructions) return } if tx.bind != nil { program, err = tx.GetAccount(uint8(tx.bind.Transaction.Message.Instructions[idx].ProgramIdIndex)) if err != nil { return } accounts = tx.bind.Transaction.Message.Instructions[idx].Accounts data = tx.bind.Transaction.Message.Instructions[idx].Data } else if len(tx.bindArray) > 0 { instr := tx.Instrs[idx] program, err = tx.GetAccount(instr.ProgramIDIndex) if err != nil { return } if instr.AccountsLen > 0 { accounts = tx.bindArray[instr.AccountsOffset : instr.AccountsOffset+instr.AccountsLen] } if instr.DataLen > 0 { data = tx.bindArray[instr.DataOffset : instr.DataOffset+instr.DataLen] } } else { err = fmt.Errorf("instruction index %d out of range, len=%d", idx, len(tx.bindArray)) return } return } func (tx *versionedTransaction) GetAddressTableLookups(idx int) (account solana.PublicKey, write []uint8, read []uint8, err error) { if idx < 0 || idx >= tx.addressTableLookups { err = fmt.Errorf("instruction index %d out of range, len=%d", idx, tx.instructions) return } if tx.bind != nil { account = solana.PublicKey(tx.bind.Transaction.Message.AddressTableLookups[idx].AccountKey) write = tx.bind.Transaction.Message.AddressTableLookups[idx].WritableIndexes read = tx.bind.Transaction.Message.AddressTableLookups[idx].ReadonlyIndexes } else if len(tx.bindArray) > 0 { lookup := tx.ATL[idx] copy(account[:], tx.bindArray[lookup.AccountKeyOffset:lookup.AccountKeyOffset+32]) if lookup.WriteLen > 0 { write = tx.bindArray[lookup.WriteOffset : lookup.WriteOffset+lookup.WriteLen] } if lookup.ReadLen > 0 { read = tx.bindArray[lookup.ReadOffset : lookup.ReadOffset+lookup.ReadLen] } } else { err = fmt.Errorf("instruction index %d out of range, len=%d", idx, len(tx.bindArray)) } return } func (tx *versionedTransaction) GetAddressTablesLookupsLen() int { return tx.addressTableLookups } func (tx *versionedTransaction) GetStaticAccountKeys(idx uint8) (account solana.PublicKey, err error) { if idx < 0 || idx >= tx.staticAccountKeys { return solana.PublicKey{}, fmt.Errorf("static account index %d out of range, len=%d", idx, tx.staticAccountKeys) } if tx.bind != nil { var key solana.PublicKey copy(key[:], tx.bind.Transaction.Message.AccountKeys[idx]) return key, nil } else if len(tx.bindArray) > 0 { start := tx.staticAccountKeysOffset + int(idx)*32 end := start + 32 var key solana.PublicKey copy(key[:], tx.bindArray[start:end]) return key, nil } else { return solana.PublicKey{}, fmt.Errorf("static account index %d out of range, len=%d", idx, len(tx.bindArray)) } } func (tx *versionedTransaction) GetStaticAccountKeysLen() uint8 { return tx.staticAccountKeys } type versionedTransaction struct { bind *SubscribeUpdateTransaction bindArray []byte signatures int signaturesOffset int instructions int addressTableLookups int staticAccountKeys uint8 staticAccountKeysOffset int Instrs []compiledInstruction ATL []addressTableLookup TableLookups []solana.PublicKey block uint64 Time time.Time } func (tx *versionedTransaction) Signatures() string { if tx.bind != nil { return base58.Encode(tx.bind.Transaction.Signatures[0]) } else if len(tx.bindArray) > 0 && tx.signatures > 0 { start := tx.signaturesOffset end := start + 64 return base58.Encode(tx.bindArray[start:end]) } return "" } func (tx *versionedTransaction) Block() uint64 { return tx.block } 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 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{} ) type compiledInstruction struct { ProgramIDIndex uint8 AccountsLen int AccountsOffset int DataOffset int DataLen int } func requireVersionedPool() *versionedTransaction { v := versionedPool.Get() if v == nil { return &versionedTransaction{ TableLookups: make([]solana.PublicKey, 0, 16), Instrs: make([]compiledInstruction, 0, 16), ATL: make([]addressTableLookup, 0, 4), } } return v.(*versionedTransaction) } func releaseVersionedPool(v *versionedTransaction) { if v == nil { return } v.TableLookups = v.TableLookups[:0] v.Instrs = v.Instrs[:0] v.ATL = v.ATL[:0] v.bind = nil v.bindArray = nil v.signaturesOffset = 0 v.signatures = 0 v.instructions = 0 v.addressTableLookups = 0 v.staticAccountKeys = 0 v.staticAccountKeysOffset = 0 v.block = 0 versionedPool.Put(v) } var VoteProgram = solana.MustPublicKeyFromBase58("Vote111111111111111111111111111111111111111") type Stats struct { Start time.Time FEC time.Time Decoded time.Time Filter time.Time Done time.Time DataLen int TxCount int TxOffset int } // ParseEntries mirrors the Rust parse_transaction entry point. func ParseEntries(slot uint64, entries []byte, loader *AddressTables, txCh chan<- TxSignal, stats bool) { var stat Stats if stats { stat.Start = time.Now() stat.FEC = time.Now() } versioned, err := entriesToVersionedTransaction(slot, newConstArray(entries)) if err != nil || len(versioned) == 0 { if err != nil { logger.Warn("decoder: failed to parse entries to versioned transactions", "slot", slot, "error", err) for _, v := range versioned { releaseVersionedPool(v) } } return } // logger.Info("parsed entries to versioned transactions", "time", stat.Start.String(), "slot", slot, "txCount", len(versioned)) if stats { stat.DataLen = len(entries) stat.TxCount = len(versioned) } //defer func() { // for _, v := range versioned { // releaseVersionedPool(v) // } //}() stat.Decoded = time.Now() for k, vTx := range versioned { //if vTx.instructions >= 1 { // programKey, _, _, _ := vTx.GetInstruction(0) // if programKey.Equals(VoteProgram) && len(vTx.TableLookups) == 0 { // releaseVersionedPool(vTx) // return // } //} go func(v *versionedTransaction, st Stats) { defer func() { releaseVersionedPool(v) }() include := false for i := uint8(0); i < v.staticAccountKeys; i++ { key, _ := v.GetAccount(i) _, include = slices.BinarySearchFunc(parseProgram, key, func(key solana.PublicKey, key2 solana.PublicKey) int { return bytes.Compare(key[:], key2[:]) }) if include { break } } if !include { return } st.Filter = time.Now() st.TxOffset = k // logger.Info("decode time", "tx", vTx.Signatures(), "s", time.Since(st.Start).String(), "idx", k, "txCount", len(versioned), "datLen", len(entries)) parseTransaction(st, v, loader, txCh) }(vTx, stat) } return } // ParseTransaction mirrors the Rust parse_transaction entry point. func ParseTransaction(update *SubscribeUpdateTransaction, loader *AddressTables, txCh chan<- TxSignal, stats bool) { var stat Stats if stats { stat.Start = time.Now() } versioned, err := toVersionedTransaction(update) if err != nil || versioned == nil || versioned.signatures == 0 { return } defer func() { releaseVersionedPool(versioned) }() parseTransaction(stat, versioned, loader, txCh) } type Handler struct { Func func(tx TransactionGetter, accounts []uint8, data []byte) (*TxSignal, error) Label string } var ( parsedMap = map[solana.PublicKey]Handler{ pumpProgramID: Handler{parsePumpInstruction, "pump"}, azczProgramID: Handler{parseAzczInstruction, "azcz"}, f5tfProgramID: Handler{parseF5tfInstruction, "f5tf"}, flasProgramID: Handler{parseFlasInstruction, "flas"}, photonProgramID: Handler{parsePhotonInstruction, "photon"}, pumpAmmProgramID: Handler{parsePumpAmmInstruction, "pumpamm"}, boboProgramID: Handler{parseBoboInstruction, "bobo"}, qtkvProgramID: Handler{parseQtkvInstruction, "qtkv"}, fjszProgramID: Handler{parseFjszInstruction, "fjsz"}, terminalProgramID: Handler{parseTermInstruction, "terminal"}, jupiterV6ProgramID: Handler{parseJupiterV6Instruction, "jupiterv6"}, okxDexRouteV2ProgramID: Handler{parseOkxDexRouteV2Instruction, "okxdexroutev2"}, dflowProgramID: Handler{parseDFlowInstruction, "dflow"}, gmgnProgramID: Handler{parseGMGNInstruction, "gmgn"}, bonkProgramID: Handler{parseBonkInstruction, "bonk"}, } parseProgram []solana.PublicKey ) func init() { for account := range parsedMap { parseProgram = append(parseProgram, account) } slices.SortFunc(parseProgram, func(a, b solana.PublicKey) int { return bytes.Compare(a[:], b[:]) }) } func parseTransaction(stat Stats, versioned *versionedTransaction, loader *AddressTables, parsed chan<- TxSignal) { // staticKeys := versioned.Message.StaticAccountKeys if loader != nil && versioned.addressTableLookups > 0 { lookupTableOk := true for i := 0; i < versioned.addressTableLookups; i++ { lookups, idxs, _, err := versioned.GetAddressTableLookups(i) if err != nil { continue } if len(idxs) == 0 { continue } lookupTableOk = loader.FillToTx(versioned, lookups, idxs) if !lookupTableOk { break } } if lookupTableOk { for i := 0; i < versioned.addressTableLookups; i++ { lookups, _, idxs, err := versioned.GetAddressTableLookups(i) if err != nil { continue } if len(idxs) == 0 { continue } lookupTableOk = loader.FillToTx(versioned, lookups, idxs) if !lookupTableOk { break } } } // versioned.Message.StaticAccountKeys = staticKeys } //instructions := versioned.GetInstruction() for i := 0; i < versioned.GetInstructionLen(); i++ { programID, accounts, data, err := versioned.GetInstruction(i) if err != nil { continue } handler, ok := parsedMap[programID] if !ok { continue } txRes, err := handler.Func(versioned, accounts, data) appendParsed(stat, parsed, txRes, err, versioned, handler.Label) } return } func appendParsed(stat Stats, txCh chan<- TxSignal, parsed *TxSignal, err error, tx TransactionGetter, label string) { if err != nil { if !strings.HasPrefix(err.Error(), "account index") { logger.Debug("txparser: failed to parse", "label", label, "instruction", err, "tx_hash", tx.Signatures()) } return } if parsed != nil { parsed.Label = label if !stat.Start.IsZero() { stat.Done = time.Now() parsed.Stats = stat } txCh <- *parsed } else { // logger.Debug("txparser: no parsed result", "label", label, "tx_hash", tx.Signatures()) } return } 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 = len(protoTx.Signatures) versioned.instructions = len(msg.Instructions) versioned.addressTableLookups = len(msg.AddressTableLookups) versioned.staticAccountKeys = uint8(len(msg.AccountKeys)) versioned.bind = update 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 parsePumpInstruction(tx TransactionGetter, accounts []uint8, data []byte) (*TxSignal, error) { if len(data) < 8 { return nil, fmt.Errorf("data is empty") } if matchMethod(data[0:8], pumpBuyV2TokensIX) || matchMethod(data[0:8], pumpBuyTokensIX) { return parsePumpBuy(tx, accounts, data) } else if matchMethod(data[0:8], pumpExtendedSellIX) { return parsePumpSell(tx, accounts, data) } else if matchMethod(data[0:8], pumpCreateCoinIX) { return parsePumpCreate(tx, accounts, data) } else if matchMethod(data[0:8], pumpCreateCoinV2IX) { return parsePumpCreateV2(tx, accounts, data) } return nil, nil } func parsePumpCreate(tx TransactionGetter, accounts []uint8, data []byte) (*TxSignal, error) { if len(accounts) < 8 { return nil, fmt.Errorf("accounts too short") } mint, err := tx.GetAccount(accounts[0]) //getStaticKey(staticKeys, int(instruction.Accounts[0])) if err != nil { return nil, err } creator, err := tx.GetAccount(accounts[7]) //getStaticKey(staticKeys, int(instruction.Accounts[7])) if err != nil { return nil, err } return &TxSignal{ TxHash: tx.Signatures(), 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 TransactionGetter, accounts []uint8, data []byte) (*TxSignal, error) { if len(accounts) < 8 { return nil, fmt.Errorf("accounts too short") } if len(data) < 8 { return nil, fmt.Errorf("data too short for pump create v2 args, len=%d", len(data)) } mint, err := tx.GetAccount(accounts[0]) //getStaticKey(staticKeys, int(instruction.Accounts[0])) if err != nil { return nil, err } tokenProgramKey, err := tx.GetAccount(accounts[7]) //getStaticKey(staticKeys, int(instruction.Accounts[7])) if err != nil { return nil, err } var args pumpCreateCoinV2Args if err := borsh.Deserialize(&args, data[8:]); err != nil { return nil, fmt.Errorf("failed to parse create coin v2 args: %w", err) } return &TxSignal{ TxHash: tx.Signatures(), 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 TransactionGetter, accounts []uint8, data []byte) (*TxSignal, error) { amount, sol, err := decodePumpBuyArgs(data) if err != nil { return nil, err } exactIn := false if matchMethod(data, pumpBuyV2TokensIX) { temp := amount amount = sol sol = temp exactIn = true } if len(accounts) < 7 { return nil, fmt.Errorf("accounts too short") } mint, err := tx.GetAccount(accounts[2]) if err != nil { return nil, err } buyer, err := tx.GetAccount(accounts[6]) if err != nil { return nil, err } return &TxSignal{ TxHash: tx.Signatures(), 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 TransactionGetter, accounts []uint8, data []byte) (*TxSignal, error) { amount, minSol, err := decodePumpSellArgs(data) if err != nil { return nil, err } if len(accounts) < 7 { return nil, fmt.Errorf("accounts too short") } mint, err := tx.GetAccount(accounts[2]) if err != nil { return nil, err } seller, err := tx.GetAccount(accounts[6]) if err != nil { return nil, err } return &TxSignal{ TxHash: tx.Signatures(), 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 TransactionGetter, accounts []uint8, data []byte) (*TxSignal, error) { if len(data) == 0 { return nil, fmt.Errorf("data is empty") } if matchMethod(data, azczBuyTokensIX) { return parseAzczBuy(tx, accounts, data) } else if matchMethod(data, azczAmmBuyTokensIX) { return parseAzczAmmBuy(tx, accounts, data) } return nil, nil } func parseAzczAmmBuy(tx TransactionGetter, accounts []uint8, data []byte) (*TxSignal, error) { if len(accounts) < 8 { return nil, fmt.Errorf("accounts too short") } mint, err := tx.GetAccount(accounts[3]) // getStaticKey(staticKeys, int(instruction.Accounts[3])) if err != nil { return nil, err } user, err := tx.GetAccount(accounts[1]) //getStaticKey(staticKeys, int(instruction.Accounts[1])) if err != nil { return nil, err } if len(data) < 17 { return nil, fmt.Errorf("data too short for azcz amm buy args, len=%d", len(data)) } solAmount := binary.LittleEndian.Uint64(data[1:9]) return &TxSignal{ TxHash: tx.Signatures(), 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 TransactionGetter, accounts []uint8, data []byte) (*TxSignal, error) { if len(accounts) < 8 { return nil, fmt.Errorf("accounts too short") } mint, err := tx.GetAccount(accounts[3]) //getStaticKey(staticKeys, int(instruction.Accounts[3])) if err != nil { return nil, err } user, err := tx.GetAccount(accounts[7]) //getStaticKey(staticKeys, int(instruction.Accounts[7])) if err != nil { return nil, err } if len(data) < 2 { return nil, fmt.Errorf("data too short for azcz buy args len=%d", len(data)) } var args azczBuyArgs if err := borsh.Deserialize(&args, data[1:]); err != nil { return nil, fmt.Errorf("failed to parse buy tokens args: %w", err) } return &TxSignal{ TxHash: tx.Signatures(), 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 TransactionGetter, accounts []uint8, data []byte) (*TxSignal, error) { if len(data) == 0 { return nil, fmt.Errorf("data is empty") } if !matchMethod(data, f5tfBuyTokensIX) { return nil, nil } if len(accounts) < 7 { return nil, fmt.Errorf("accounts too short") } mint, err := tx.GetAccount(accounts[2]) //getStaticKey(staticKeys, int(instruction.Accounts[2])) if err != nil { return nil, err } user, err := tx.GetAccount(accounts[6]) //getStaticKey(staticKeys, int(instruction.Accounts[6])) if err != nil { return nil, err } if len(data) < 2 { return nil, fmt.Errorf("data too short for f5tf buy args len=%d", len(data)) } var args f5tfBuyArgs if err := borsh.Deserialize(&args, data[1:]); err != nil { return nil, fmt.Errorf("failed to parse buy tokens args: %w", err) } return &TxSignal{ TxHash: tx.Signatures(), 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 TransactionGetter, accounts []uint8, data []byte) (*TxSignal, error) { if len(data) == 0 { return nil, fmt.Errorf("data is empty") } if len(data) == 10 && data[0] == 1 { return nil, nil } if len(data) < 20 { return nil, fmt.Errorf("data too short for args flas instruction, len: %d", len(data)) } methodData := data[17:20] //if !matchMethod(methodData, flasBuyTokensIX) { // return nil, nil //} if matchMethod(methodData, flasBuyTokensIX) { return parseFlasBuy(tx, accounts, data) } else if matchMethod(methodData, flasSellTokensIX) { return parseFlasSell(tx, accounts, data) } else if matchMethod(methodData, flasAmmBuyTokensIX) { return parseFlasAmmBuy(tx, accounts, data) } else if matchMethod(methodData, flasAmmSellTokensIX) { return parseFlasAmmSell(tx, accounts, data) } return nil, nil } func parseFlasAmmSell(tx TransactionGetter, accounts []uint8, data []byte) (*TxSignal, error) { if len(accounts) < 10 { return nil, fmt.Errorf("accounts too short") } mint, err := tx.GetAccount(accounts[9]) //getStaticKey(staticKeys, int(instruction.Accounts[9])) if err != nil { return nil, err } user, err := tx.GetAccount(accounts[1]) // getStaticKey(staticKeys, int(instruction.Accounts[1])) if err != nil { return nil, err } var args flasArgs if err := borsh.Deserialize(&args, data[1:]); err != nil { return nil, fmt.Errorf("failed to parse buy tokens args: %w", err) } return &TxSignal{ TxHash: tx.Signatures(), 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 TransactionGetter, accounts []uint8, data []byte) (*TxSignal, error) { if len(accounts) < 10 { return nil, fmt.Errorf("accounts too short") } mint, err := tx.GetAccount(accounts[9]) //getStaticKey(staticKeys, int(instruction.Accounts[9])) if err != nil { return nil, err } user, err := tx.GetAccount(accounts[1]) // getStaticKey(staticKeys, int(instruction.Accounts[1])) if err != nil { return nil, err } var args flasArgs if err := borsh.Deserialize(&args, data[1:]); err != nil { return nil, fmt.Errorf("failed to parse buy tokens args: %w", err) } return &TxSignal{ TxHash: tx.Signatures(), 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 TransactionGetter, accounts []uint8, data []byte) (*TxSignal, error) { if len(accounts) < 9 { return nil, fmt.Errorf("accounts too short") } mint, err := tx.GetAccount(accounts[8]) //getStaticKey(staticKeys, int(instruction.Accounts[8])) if err != nil { return nil, err } user, err := tx.GetAccount(accounts[1]) //getStaticKey(staticKeys, int(instruction.Accounts[1])) if err != nil { return nil, err } var args flasArgs if err := borsh.Deserialize(&args, data[1:]); err != nil { return nil, fmt.Errorf("failed to parse buy tokens args: %w", err) } return &TxSignal{ TxHash: tx.Signatures(), 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 TransactionGetter, accounts []uint8, data []byte) (*TxSignal, error) { if len(accounts) < 9 { return nil, fmt.Errorf("accounts too short") } mint, err := tx.GetAccount(accounts[8]) //getStaticKey(staticKeys, int(instruction.Accounts[8])) if err != nil { return nil, err } user, err := tx.GetAccount(accounts[0]) //getStaticKey(staticKeys, int(instruction.Accounts[0])) if err != nil { return nil, err } if len(data) > 20 { data = data[:20] } var args flasArgs if err := borsh.Deserialize(&args, data[1:]); err != nil { return nil, fmt.Errorf("failed to parse buy tokens args: %w", err) } return &TxSignal{ TxHash: tx.Signatures(), 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 TransactionGetter, accounts []uint8, data []byte) (*TxSignal, error) { if len(data) == 0 { return nil, fmt.Errorf("data is empty") } if len(data) < 8 { return nil, nil } if matchMethod(data, gmgnBuyTokensIX) { return parseGMGNBuy(tx, accounts, data) } return nil, nil } func parseGMGNBuy(tx TransactionGetter, accounts []uint8, data []byte) (*TxSignal, error) { if len(accounts) < 8 { return nil, fmt.Errorf("accounts too short") } if len(data) < 24 { return nil, fmt.Errorf("data too short for gmgn buy args, len=%d", len(data)) } mint, err := tx.GetAccount(accounts[2]) //getStaticKey(staticKeys, int(instruction.Accounts[2])) if err != nil { return nil, err } user, err := tx.GetAccount(accounts[6]) //getStaticKey(staticKeys, int(instruction.Accounts[6])) if err != nil { return nil, err } solAmount := binary.LittleEndian.Uint64(data[8:16]) tokenAmount := binary.LittleEndian.Uint64(data[16:24]) return &TxSignal{ TxHash: tx.Signatures(), 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 TransactionGetter, accounts []uint8, data []byte) (*TxSignal, error) { if len(data) == 0 { return nil, fmt.Errorf("data is empty") } if len(data) < 8 { return nil, nil } switch { case bytes.Equal(data[:8], photonBuyPumpTokensIX): return parsePhotonBuy(tx, accounts, data) case bytes.Equal(data[:8], photonSwapPumpAmmIX): return parsePhotonSwap(tx, accounts, data) default: return nil, nil } } func parsePhotonBuy(tx TransactionGetter, accounts []uint8, data []byte) (*TxSignal, error) { if len(accounts) < 8 { return nil, fmt.Errorf("accounts too short") } if len(data) < 16 { return nil, fmt.Errorf("data too short for photon buy args, len=%d", len(data)) } mint, err := tx.GetAccount(accounts[3]) //getStaticKey(staticKeys, int(instruction.Accounts[3])) if err != nil { return nil, err } user, err := tx.GetAccount(accounts[7]) //getStaticKey(staticKeys, int(instruction.Accounts[7])) if err != nil { return nil, err } var args photonBuyPumpArgs if err := borsh.Deserialize(&args, 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(), 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 TransactionGetter, accounts []uint8, data []byte) (*TxSignal, error) { if len(accounts) < 8 { return nil, fmt.Errorf("accounts too short") } if len(data) < 16 { return nil, fmt.Errorf("data too short for swap args for photon. len=%d", len(data)) } base, err := tx.GetAccount(accounts[3]) //getStaticKey(staticKeys, int(instruction.Accounts[3])) if err != nil { return nil, err } quote, err := tx.GetAccount(accounts[4]) //getStaticKey(staticKeys, int(instruction.Accounts[4])) if err != nil { return nil, err } if !quote.Equals(solana.WrappedSol) { return nil, nil } buyer, err := tx.GetAccount(accounts[1]) //getStaticKey(staticKeys, int(instruction.Accounts[1])) if err != nil { return nil, err } var args photonSwapPumpAmmArgs if err := borsh.Deserialize(&args, 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(), 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 TransactionGetter, accounts []uint8, data []byte) (*TxSignal, error) { if len(data) == 0 { return nil, fmt.Errorf("data is empty") } if matchMethod(data, pumpAmmBuyTokensIX) || matchMethod(data, pumpAmmBuyTokensV2IX) { return parsePumpAmmBuy(tx, accounts, data) } else if matchMethod(data, pumpAmmSellTokensIX) { return parsePumpAmmSell(tx, accounts, data) } return nil, nil } func parseTermInstruction(tx TransactionGetter, accounts []uint8, data []byte) (*TxSignal, error) { if len(data) == 0 { return nil, fmt.Errorf("data is empty") } if len(data) < 24 { return nil, nil } switch { case bytes.Equal(data[:8], terminalBuyTokensIX): return parseTermBuy(tx, accounts, data) case bytes.Equal(data[:8], terminalSellTokensIX): return parseTermSell(tx, accounts, data) case bytes.Equal(data[:8], terminalAmmSellTokensIX): return parseTermAmmSell(tx, accounts, data) default: return nil, nil } } func parseTermAmmSell(tx TransactionGetter, accounts []uint8, data []byte) (*TxSignal, error) { if len(accounts) < 8 { return nil, fmt.Errorf("accounts too short") } mint, err := tx.GetAccount(accounts[3]) //getStaticKey(staticKeys, int(instruction.Accounts[3])) if err != nil { return nil, err } user, err := tx.GetAccount(accounts[1]) //getStaticKey(staticKeys, int(instruction.Accounts[1])) if err != nil { return nil, err } solAmount := binary.LittleEndian.Uint64(data[8:16]) tokenAmount := binary.LittleEndian.Uint64(data[16:24]) return &TxSignal{ TxHash: tx.Signatures(), 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 TransactionGetter, accounts []uint8, data []byte) (*TxSignal, error) { if len(accounts) < 8 { return nil, fmt.Errorf("accounts too short") } mint, err := tx.GetAccount(accounts[2]) // getStaticKey(staticKeys, int(instruction.Accounts[2])) if err != nil { return nil, err } user, err := tx.GetAccount(accounts[6]) // getStaticKey(staticKeys, int(instruction.Accounts[6])) if err != nil { return nil, err } solAmount := binary.LittleEndian.Uint64(data[8:16]) tokenAmount := binary.LittleEndian.Uint64(data[16:24]) return &TxSignal{ TxHash: tx.Signatures(), 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 TransactionGetter, accounts []uint8, data []byte) (*TxSignal, error) { if len(accounts) < 8 { return nil, fmt.Errorf("accounts too short") } mint, err := tx.GetAccount(accounts[2]) //getStaticKey(staticKeys, int(instruction.Accounts[2])) if err != nil { return nil, err } user, err := tx.GetAccount(accounts[6]) //getStaticKey(staticKeys, int(instruction.Accounts[6])) if err != nil { return nil, err } tokenAmount := binary.LittleEndian.Uint64(data[8:16]) solAmount := binary.LittleEndian.Uint64(data[16:24]) return &TxSignal{ TxHash: tx.Signatures(), 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 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 TransactionGetter, accounts []uint8, data []byte) (*TxSignal, error) { amount, maxSol, err := decodePumpAmmBuyArgs(data) if err != nil { return nil, err } exactIn := false if matchMethod(data, pumpAmmBuyTokensV2IX) { temp := amount amount = maxSol maxSol = temp exactIn = true } if len(accounts) < 7 { return nil, fmt.Errorf("accounts too short") } base, err := tx.GetAccount(accounts[3]) //getStaticKey(staticKeys, int(instruction.Accounts[3])) if err != nil { return nil, err } quote, err := tx.GetAccount(accounts[4]) //getStaticKey(staticKeys, int(instruction.Accounts[4])) if err != nil { return nil, err } if !quote.Equals(solana.WrappedSol) { return nil, nil } buyer, err := tx.GetAccount(accounts[1]) //getStaticKey(staticKeys, int(instruction.Accounts[1])) if err != nil { return nil, err } return &TxSignal{ TxHash: tx.Signatures(), 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 TransactionGetter, accounts []uint8, data []byte) (*TxSignal, error) { amount, minSol, err := decodePumpAmmBuyArgs(data) if err != nil { return nil, err } if len(accounts) < 7 { return nil, fmt.Errorf("accounts too short") } base, err := tx.GetAccount(accounts[3]) // getStaticKey(staticKeys, int(instruction.Accounts[3])) if err != nil { return nil, err } quote, err := tx.GetAccount(accounts[4]) //getStaticKey(staticKeys, int(instruction.Accounts[4])) if err != nil { return nil, err } if !quote.Equals(solana.WrappedSol) { return nil, nil } buyer, err := tx.GetAccount(accounts[1]) //getStaticKey(staticKeys, int(instruction.Accounts[1])) if err != nil { return nil, err } return &TxSignal{ TxHash: tx.Signatures(), 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 TransactionGetter, accounts []uint8, data []byte) (*TxSignal, error) { if len(data) == 0 { return nil, fmt.Errorf("data is empty") } if len(data) < 8 || !bytes.Equal(data[:8], boboBuyPumpTokensIX) { return nil, nil } if len(accounts) < 8 { return nil, fmt.Errorf("accounts too short") } if len(data) < 16 { return nil, fmt.Errorf("data too short for bobo buy args, len=%d", len(data)) } mint, err := tx.GetAccount(accounts[2]) //getStaticKey(staticKeys, int(instruction.Accounts[2])) if err != nil { return nil, err } user, err := tx.GetAccount(accounts[6]) // getStaticKey(staticKeys, int(instruction.Accounts[6])) if err != nil { return nil, err } var args boboBuyArgs if err := borsh.Deserialize(&args, data[8:]); err != nil { return nil, fmt.Errorf("failed to parse buy tokens args: %w", err) } return &TxSignal{ TxHash: tx.Signatures(), 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 TransactionGetter, accounts []uint8, data []byte) (*TxSignal, error) { if len(data) == 0 { return nil, fmt.Errorf("data is empty") } if matchMethod(data, qtkvBuyTokensIX) { return parseQtkvBuy(tx, accounts, data) } else if matchMethod(data, qtkvAmmSellTokensIX) { return parseQtkvAmmSell(tx, accounts, data) } else if matchMethod(data, qtkvSellTokensIX) { return parseQtkvSell(tx, accounts, data) } return nil, nil } func parseQtkvSell(tx TransactionGetter, accounts []uint8, data []byte) (*TxSignal, error) { if len(accounts) < 11 { return nil, fmt.Errorf("accounts too short") } if len(data) < 24 { return nil, fmt.Errorf("data too short for qtkv sell args, len=%d", len(data)) } mint, err := tx.GetAccount(accounts[10]) //getStaticKey(staticKeys, int(instruction.Accounts[10])) if err != nil { return nil, err } user, err := tx.GetAccount(accounts[0]) //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(data[19:25]) return &TxSignal{ TxHash: tx.Signatures(), 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 TransactionGetter, accounts []uint8, data []byte) (*TxSignal, error) { if len(accounts) < 11 { return nil, fmt.Errorf("accounts too short") } if len(data) < 24 { return nil, fmt.Errorf("data too short for qtkv amm sell args, len=%d", len(data)) } mint, err := tx.GetAccount(accounts[10]) //getStaticKey(staticKeys, int(instruction.Accounts[10])) if err != nil { return nil, err } user, err := tx.GetAccount(accounts[0]) //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(data[19:25]) return &TxSignal{ TxHash: tx.Signatures(), 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 TransactionGetter, accounts []uint8, data []byte) (*TxSignal, error) { if len(accounts) < 8 { return nil, fmt.Errorf("accounts too short") } mint, err := tx.GetAccount(accounts[3]) //getStaticKey(staticKeys, int(instruction.Accounts[3])) if err != nil { return nil, err } user, err := tx.GetAccount(accounts[0]) // getStaticKey(staticKeys, int(instruction.Accounts[0])) if err != nil { return nil, err } var args qtkvBuyArgs if err := borsh.Deserialize(&args, data[1:]); err != nil { return nil, fmt.Errorf("failed to parse buy tokens args: %w", err) } return &TxSignal{ TxHash: tx.Signatures(), 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 TransactionGetter, accounts []uint8, data []byte) (*TxSignal, error) { if len(data) == 0 { return nil, fmt.Errorf("data is empty") } if !matchMethod(data, fjszBuyTokensIX) { return nil, nil } if len(accounts) < 7 { return nil, fmt.Errorf("accounts too short") } if len(data) < 16 { return nil, fmt.Errorf("data too short for fjzs buy args, len=%d", len(data)) } mint, err := tx.GetAccount(accounts[2]) //getStaticKey(staticKeys, int(instruction.Accounts[2])) if err != nil { return nil, err } user, err := tx.GetAccount(accounts[6]) //getStaticKey(staticKeys, int(instruction.Accounts[6])) if err != nil { return nil, err } var args fjszBuyArgs if err := borsh.Deserialize(&args, data[8:]); err != nil { return nil, fmt.Errorf("failed to parse buy tokens args: %w", err) } return &TxSignal{ TxHash: tx.Signatures(), 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 TransactionGetter, accounts []uint8, data []byte) (*TxSignal, error) { if len(data) == 0 { return nil, fmt.Errorf("data is empty") } if matchMethod(data, bonkBuyAndSellTokensIX) { return parseBonkBuyAndSell(tx, accounts, data) } return nil, nil } func parseBonkBuyAndSell(tx TransactionGetter, accounts []uint8, data []byte) (*TxSignal, error) { if len(accounts) < 8 { return nil, fmt.Errorf("accounts too short") } programId, err := tx.GetAccount(accounts[7]) //getStaticKey(staticKeys, int(instruction.Accounts[7])) if err != nil { return nil, err } if programId != pumpProgramID { return nil, nil } user, err := tx.GetAccount(accounts[0]) //getStaticKey(staticKeys, int(instruction.Accounts[0])) if err != nil { return nil, err } flagAccount, err := tx.GetAccount(accounts[4]) //getStaticKey(staticKeys, int(instruction.Accounts[4])) if err != nil { return nil, err } amount1 := binary.LittleEndian.Uint64(data[17:25]) amount2 := binary.LittleEndian.Uint64(data[25:33]) if user == flagAccount { mint, err := tx.GetAccount(accounts[6]) // getStaticKey(staticKeys, int(instruction.Accounts[6])) if err != nil { return nil, err } return &TxSignal{ TxHash: tx.Signatures(), 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 := tx.GetAccount(accounts[5]) //getStaticKey(staticKeys, int(instruction.Accounts[5])) if err != nil { return nil, err } return &TxSignal{ TxHash: tx.Signatures(), 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 matchMethod(data []byte, methods []byte) bool { if len(data) < len(methods) { return false } return bytes.Equal(data[0:len(methods)], methods) }