Files
libsam/pkg/shreder/txparser.go
2026-01-28 18:42:34 +08:00

255 lines
7.9 KiB
Go

package shreder
import (
"bytes"
"context"
"fmt"
"io"
"math/big"
"slices"
"strings"
"github.com/gagliardetto/solana-go"
"github.com/shopspring/decimal"
)
const (
wsolMint = "So11111111111111111111111111111111111111112"
tokenProgram = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
)
type Handler struct {
Func func(tx VersionedTransaction, idx int) (TxSignalBatch, error)
Label string
}
type FillAccount interface {
FillAccount(account solana.PublicKey)
}
func init() {
for account := range parsedMap {
parseProgram = append(parseProgram, account)
}
//"GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR", //Event Authority
//"5PHirr8joyTMp9JMm6nW7hNDVyEYdkzDqazxPD7RaTjx", // Fee Config
//"pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ", // pump fee program
parseProgram = append(parseProgram,
solana.MustPublicKeyFromBase58("GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR"),
solana.MustPublicKeyFromBase58("5PHirr8joyTMp9JMm6nW7hNDVyEYdkzDqazxPD7RaTjx"),
solana.MustPublicKeyFromBase58("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ"),
)
slices.SortFunc(parseProgram, func(a, b solana.PublicKey) int {
return bytes.Compare(a[:], b[:])
})
}
var (
parseProgram []solana.PublicKey
parsedMap = map[solana.PublicKey]Handler{
pumpProgramID: {parsePumpInstruction, "pump"},
azczProgramID: {parseAzczInstruction, "azcz"},
f5tfProgramID: {parseF5tfInstruction, "f5tf"},
flasProgramID: {parseFlasInstruction, "flas"},
photonProgramID: {parsePhotonInstruction, "photon"},
pumpAmmProgramID: {parsePumpAmmInstruction, "pumpamm"},
boboProgramID: {parseBoboInstruction, "bobo"},
qtkvProgramID: {parseQtkvInstruction, "qtkv"},
fjszProgramID: {parseFjszInstruction, "fjsz"},
terminalProgramID: {parseTermInstruction, "terminal"},
jupiterV6ProgramID: {parseJupiterV6Instruction, "jupiterv6"},
okxDexRouteV2ProgramID: {parseOkxDexRouteV2Instruction, "okxdexroutev2"},
dflowProgramID: {parseDFlowInstruction, "dflow"},
gmgnProgramID: {parseGMGNInstruction, "gmgn"},
bonkProgramID: {parseBonkInstruction, "bonk"},
bloomRouterProgramID: {parseBloomRouterInstruction, "bloomrouter"},
dlmmProgramID: {parseDlmmInstruction, "dlmm"},
}
)
func ParseTransactionForSubscribe(ctx context.Context, update *SubscribeUpdateTransaction, loader *AddressTables, parsed chan<- TxSignal, done chan<- struct{}) {
versioned, err := toVersionedTransaction(update)
if err != nil {
logger.Debug("txparser: failed to convert to versioned transaction", "error", err)
if done != nil {
close(done)
}
return
}
ParseTransaction(ctx, versioned, loader, parsed)
if done != nil {
close(done)
}
}
var VoteProgram = solana.MustPublicKeyFromBase58("Vote111111111111111111111111111111111111111")
func FilterTransactionForEntries(versioned VersionedTransaction) bool {
if len(versioned.Instructions) >= 1 {
programKey, _ := versioned.GetAccount(int(versioned.Instructions[0].ProgramIDIndex))
if programKey.Equals(VoteProgram) && len(versioned.AddressTableLookups) == 0 {
return true
}
}
// accounts filter?
include := false
for _, key := range versioned.StaticAccountKeys {
_, include = slices.BinarySearchFunc(parseProgram, key, func(key solana.PublicKey, key2 solana.PublicKey) int {
return bytes.Compare(key[:], key2[:])
})
if include {
break
}
}
return !include
}
func ParseTransactionForEntries(ctx context.Context, slot uint64, entriesReader io.Reader, loader *AddressTables, parsed chan<- TxSignal) {
err := entriesToVersionedTransaction(slot, entriesReader, func(versioned VersionedTransaction) {
// filter out vote transactions
if FilterTransactionForEntries(versioned) {
return
}
go ParseTransaction(ctx, versioned, loader, parsed)
})
if err != nil {
logger.Debug("txparser: failed to parse entries", "error", err)
return
}
}
func ParseTransaction(ctx context.Context, versioned VersionedTransaction, loader *AddressTables, parsed chan<- TxSignal) {
// staticKeys := versioned.Message.StaticAccountKeys
if loader != nil && len(versioned.AddressTableLookups) > 0 {
lookupTableOk := true
for _, lookups := range versioned.AddressTableLookups {
lookupTableOk = loader.FillToTx(&versioned, lookups.AccountKey, lookups.WritableIndexes)
if !lookupTableOk {
break
}
}
if lookupTableOk {
for _, lookups := range versioned.AddressTableLookups {
lookupTableOk = loader.FillToTx(&versioned, lookups.AccountKey, lookups.ReadonlyIndexes)
if !lookupTableOk {
break
}
}
}
}
for i, instruction := range versioned.Instructions {
//load from address table
program, err := versioned.GetAccount(int(instruction.ProgramIDIndex))
if err != nil {
continue
}
handler, ok := parsedMap[program]
if !ok {
continue
}
txRes, err := handler.Func(versioned, i)
if err != nil {
if !strings.HasPrefix(err.Error(), "account index") {
logger.Debug("txparser: failed to parse", "label", handler.Label, "err", err, "tx_hash", versioned.Signatures[0].String())
}
continue
}
if txRes != nil {
for _, one := range txRes {
if one == nil {
continue
}
one.Label = handler.Label
one.Block = versioned.Block
select {
case <-ctx.Done():
return
case parsed <- *one:
}
}
}
}
return
}
func toVersionedTransaction(update *SubscribeUpdateTransaction) (VersionedTransaction, error) {
if update == nil || update.Transaction == nil || update.Transaction.Message == nil {
return VersionedTransaction{}, fmt.Errorf("transaction is nil")
}
protoTx := update.Transaction
msg := protoTx.Message
versioned := VersionedTransaction{
Signatures: make([]solana.Signature, 0, 10),
StaticAccountKeys: make([]solana.PublicKey, 0, 256),
Instructions: make([]Instructions, 0, 16),
AddressTableLookups: make([]AddressTableLookup, 0, 10),
}
for _, rawSig := range protoTx.Signatures {
versioned.Signatures = append(versioned.Signatures, solana.SignatureFromBytes(rawSig))
}
versioned.StaticAccountKeys = versioned.StaticAccountKeys[:0]
for _, key := range msg.AccountKeys {
versioned.StaticAccountKeys = append(versioned.StaticAccountKeys, solana.PublicKeyFromBytes(key))
}
versioned.Instructions = versioned.Instructions[:0]
for _, instr := range msg.Instructions {
accounts := make([]uint8, 0, 16)
accounts = append(accounts, instr.Accounts...)
versioned.Instructions = append(versioned.Instructions, Instructions{
ProgramIDIndex: uint8(instr.ProgramIdIndex),
Accounts: accounts,
Data: instr.Data,
})
}
versioned.AddressTableLookups = versioned.AddressTableLookups[:0]
for _, lookup := range msg.AddressTableLookups {
writable := make([]uint8, 0, 16)
writable = append(writable, lookup.WritableIndexes...)
readonly := make([]uint8, 0, 16)
readonly = append(readonly, lookup.ReadonlyIndexes...)
versioned.AddressTableLookups = append(versioned.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 findAssociatedTokenAddressWithTokenProgram(wallet, mint, tokenProgram solana.PublicKey) (solana.PublicKey, uint8, error) {
return solana.FindProgramAddress([][]byte{
wallet[:],
tokenProgram[:],
mint[:],
}, solana.SPLAssociatedTokenAccountProgramID)
}
func matchMethod(data []byte, methods []byte) bool {
if len(data) < len(methods) {
return false
}
return bytes.Equal(data[0:len(methods)], methods)
}