2025-12-30 11:03:11 +08:00
|
|
|
package shreder
|
2025-12-26 10:57:37 +08:00
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
2026-01-28 14:11:34 +08:00
|
|
|
"context"
|
2025-12-26 10:57:37 +08:00
|
|
|
"fmt"
|
2026-01-28 14:11:34 +08:00
|
|
|
"io"
|
2025-12-26 10:57:37 +08:00
|
|
|
"math/big"
|
2026-01-28 14:11:34 +08:00
|
|
|
"slices"
|
2026-01-07 11:18:02 +08:00
|
|
|
"strings"
|
2025-12-26 10:57:37 +08:00
|
|
|
|
|
|
|
|
"github.com/gagliardetto/solana-go"
|
|
|
|
|
"github.com/shopspring/decimal"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
wsolMint = "So11111111111111111111111111111111111111112"
|
|
|
|
|
tokenProgram = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
|
|
|
|
|
)
|
|
|
|
|
|
2026-01-28 14:11:34 +08:00
|
|
|
type Handler struct {
|
|
|
|
|
Func func(tx VersionedTransaction, idx int) (TxSignalBatch, error)
|
|
|
|
|
Label string
|
2025-12-26 10:57:37 +08:00
|
|
|
}
|
|
|
|
|
|
2026-01-28 14:11:34 +08:00
|
|
|
type FillAccount interface {
|
|
|
|
|
FillAccount(account solana.PublicKey)
|
2025-12-26 10:57:37 +08:00
|
|
|
}
|
|
|
|
|
|
2026-01-28 14:11:34 +08:00
|
|
|
func init() {
|
2026-01-30 11:45:57 +08:00
|
|
|
for account := range registered {
|
|
|
|
|
defaultFilterAccount = append(defaultFilterAccount, account)
|
2026-01-28 14:11:34 +08:00
|
|
|
}
|
|
|
|
|
//"GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR", //Event Authority
|
|
|
|
|
//"5PHirr8joyTMp9JMm6nW7hNDVyEYdkzDqazxPD7RaTjx", // Fee Config
|
|
|
|
|
//"pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ", // pump fee program
|
2026-01-30 11:45:57 +08:00
|
|
|
defaultFilterAccount = append(defaultFilterAccount,
|
2026-01-28 14:11:34 +08:00
|
|
|
solana.MustPublicKeyFromBase58("GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR"),
|
|
|
|
|
solana.MustPublicKeyFromBase58("5PHirr8joyTMp9JMm6nW7hNDVyEYdkzDqazxPD7RaTjx"),
|
|
|
|
|
solana.MustPublicKeyFromBase58("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ"),
|
|
|
|
|
)
|
2026-01-30 11:45:57 +08:00
|
|
|
slices.SortFunc(defaultFilterAccount, func(a, b solana.PublicKey) int {
|
2026-01-28 14:11:34 +08:00
|
|
|
return bytes.Compare(a[:], b[:])
|
|
|
|
|
})
|
2025-12-26 10:57:37 +08:00
|
|
|
}
|
|
|
|
|
|
2026-01-30 11:45:57 +08:00
|
|
|
type FilterParams struct {
|
|
|
|
|
Require []solana.PublicKey
|
|
|
|
|
Include []solana.PublicKey
|
|
|
|
|
Exclude []solana.PublicKey
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-07 21:15:54 +08:00
|
|
|
var (
|
2026-01-30 11:45:57 +08:00
|
|
|
defaultFilterAccount []solana.PublicKey
|
2026-01-28 14:11:34 +08:00
|
|
|
|
2026-01-30 11:45:57 +08:00
|
|
|
registered = map[solana.PublicKey]Handler{
|
2026-01-28 14:11:34 +08:00
|
|
|
pumpProgramID: {parsePumpInstruction, "pump"},
|
|
|
|
|
azczProgramID: {parseAzczInstruction, "azcz"},
|
|
|
|
|
f5tfProgramID: {parseF5tfInstruction, "f5tf"},
|
|
|
|
|
flasProgramID: {parseFlasInstruction, "flas"},
|
|
|
|
|
photonProgramID: {parsePhotonInstruction, "photon"},
|
|
|
|
|
pumpAmmProgramID: {parsePumpAmmInstruction, "pumpamm"},
|
2026-01-29 16:40:32 +08:00
|
|
|
binanceWalletProgramID: {parseBinanceWalletInstruction, "binancewallet"},
|
2026-01-28 14:11:34 +08:00
|
|
|
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"},
|
2026-02-02 11:02:10 +08:00
|
|
|
dbotProgramID: {parseDbotInstruction, "dbot"},
|
2026-02-03 14:06:43 +08:00
|
|
|
tradewizProgramID: {parseTradewizInstruction, "tradewiz"},
|
2026-01-28 14:11:34 +08:00
|
|
|
}
|
2026-01-07 21:15:54 +08:00
|
|
|
)
|
|
|
|
|
|
2026-01-28 14:11:34 +08:00
|
|
|
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)
|
2026-01-28 18:42:34 +08:00
|
|
|
if done != nil {
|
|
|
|
|
close(done)
|
|
|
|
|
}
|
2026-01-28 14:11:34 +08:00
|
|
|
return
|
2026-01-07 21:15:54 +08:00
|
|
|
}
|
2026-01-28 14:11:34 +08:00
|
|
|
ParseTransaction(ctx, versioned, loader, parsed)
|
2026-01-28 18:42:34 +08:00
|
|
|
if done != nil {
|
|
|
|
|
close(done)
|
|
|
|
|
}
|
2026-01-07 21:15:54 +08:00
|
|
|
}
|
|
|
|
|
|
2026-01-28 14:11:34 +08:00
|
|
|
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
|
|
|
|
|
}
|
2026-01-07 21:15:54 +08:00
|
|
|
}
|
|
|
|
|
|
2026-01-28 14:11:34 +08:00
|
|
|
// accounts filter?
|
|
|
|
|
include := false
|
|
|
|
|
for _, key := range versioned.StaticAccountKeys {
|
2026-01-30 11:45:57 +08:00
|
|
|
_, include = slices.BinarySearchFunc(defaultFilterAccount, key, func(key solana.PublicKey, key2 solana.PublicKey) int {
|
2026-01-28 14:11:34 +08:00
|
|
|
return bytes.Compare(key[:], key2[:])
|
|
|
|
|
})
|
|
|
|
|
if include {
|
|
|
|
|
break
|
2026-01-07 21:15:54 +08:00
|
|
|
}
|
|
|
|
|
}
|
2026-01-28 14:11:34 +08:00
|
|
|
return !include
|
2026-01-07 21:15:54 +08:00
|
|
|
}
|
|
|
|
|
|
2026-01-30 11:45:57 +08:00
|
|
|
func GetRegisteredHandlers() map[solana.PublicKey]Handler {
|
|
|
|
|
return registered
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func FilterTransactionForEntriesWithFilter(versioned VersionedTransaction, filter map[string]FilterParams) bool {
|
|
|
|
|
if len(versioned.Instructions) >= 1 {
|
|
|
|
|
programKey, _ := versioned.GetAccount(int(versioned.Instructions[0].ProgramIDIndex))
|
|
|
|
|
if programKey.Equals(VoteProgram) && len(versioned.AddressTableLookups) == 0 {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, params := range filter {
|
|
|
|
|
excludePass := true
|
|
|
|
|
// exclude first
|
|
|
|
|
for _, key := range params.Exclude {
|
|
|
|
|
if slices.Contains(versioned.StaticAccountKeys, key) {
|
|
|
|
|
excludePass = false
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
requirePass := true
|
|
|
|
|
if excludePass {
|
|
|
|
|
for _, key := range params.Require {
|
|
|
|
|
if !slices.Contains(versioned.StaticAccountKeys, key) {
|
|
|
|
|
requirePass = false
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
include := len(params.Include) == 0
|
|
|
|
|
if excludePass && requirePass {
|
|
|
|
|
for _, key := range params.Include {
|
|
|
|
|
if slices.Contains(versioned.StaticAccountKeys, key) {
|
|
|
|
|
include = true
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if excludePass && requirePass && include {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-28 14:11:34 +08:00
|
|
|
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)
|
2026-01-07 21:15:54 +08:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-30 11:45:57 +08:00
|
|
|
func ParseTransactionWithHandler(ctx context.Context, versioned VersionedTransaction, loader *AddressTables, parsed chan<- TxSignal, handlers map[solana.PublicKey]Handler) {
|
2026-01-28 14:11:34 +08:00
|
|
|
if loader != nil && len(versioned.AddressTableLookups) > 0 {
|
2026-01-06 16:42:07 +08:00
|
|
|
lookupTableOk := true
|
2026-01-28 14:11:34 +08:00
|
|
|
for _, lookups := range versioned.AddressTableLookups {
|
|
|
|
|
lookupTableOk = loader.FillToTx(&versioned, lookups.AccountKey, lookups.WritableIndexes)
|
2026-01-08 11:57:57 +08:00
|
|
|
if !lookupTableOk {
|
2026-01-05 12:45:32 +08:00
|
|
|
break
|
|
|
|
|
}
|
2026-01-06 16:42:07 +08:00
|
|
|
}
|
|
|
|
|
if lookupTableOk {
|
2026-01-28 14:11:34 +08:00
|
|
|
for _, lookups := range versioned.AddressTableLookups {
|
|
|
|
|
lookupTableOk = loader.FillToTx(&versioned, lookups.AccountKey, lookups.ReadonlyIndexes)
|
2026-01-08 11:57:57 +08:00
|
|
|
if !lookupTableOk {
|
2026-01-06 16:42:07 +08:00
|
|
|
break
|
|
|
|
|
}
|
2026-01-05 12:45:32 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-28 14:11:34 +08:00
|
|
|
for i, instruction := range versioned.Instructions {
|
|
|
|
|
//load from address table
|
|
|
|
|
program, err := versioned.GetAccount(int(instruction.ProgramIDIndex))
|
|
|
|
|
if err != nil {
|
2025-12-26 10:57:37 +08:00
|
|
|
continue
|
|
|
|
|
}
|
2026-01-30 11:45:57 +08:00
|
|
|
handler, ok := handlers[program]
|
2026-01-28 14:11:34 +08:00
|
|
|
if !ok {
|
|
|
|
|
continue
|
2026-01-27 14:48:18 +08:00
|
|
|
}
|
2026-01-28 14:11:34 +08:00
|
|
|
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())
|
|
|
|
|
}
|
2026-01-27 14:48:18 +08:00
|
|
|
continue
|
|
|
|
|
}
|
2026-01-28 14:11:34 +08:00
|
|
|
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:
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-27 14:48:18 +08:00
|
|
|
}
|
2026-01-28 14:11:34 +08:00
|
|
|
|
2026-01-27 14:48:18 +08:00
|
|
|
}
|
2026-01-28 14:11:34 +08:00
|
|
|
|
|
|
|
|
return
|
2026-01-27 14:48:18 +08:00
|
|
|
}
|
|
|
|
|
|
2026-01-30 11:45:57 +08:00
|
|
|
func ParseTransaction(ctx context.Context, versioned VersionedTransaction, loader *AddressTables, parsed chan<- TxSignal) {
|
|
|
|
|
// staticKeys := versioned.Message.StaticAccountKeys
|
|
|
|
|
ParseTransactionWithHandler(ctx, versioned, loader, parsed, registered)
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-28 14:11:34 +08:00
|
|
|
func toVersionedTransaction(update *SubscribeUpdateTransaction) (VersionedTransaction, error) {
|
2025-12-26 10:57:37 +08:00
|
|
|
if update == nil || update.Transaction == nil || update.Transaction.Message == nil {
|
2026-01-28 14:11:34 +08:00
|
|
|
return VersionedTransaction{}, fmt.Errorf("transaction is nil")
|
2025-12-26 10:57:37 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protoTx := update.Transaction
|
|
|
|
|
msg := protoTx.Message
|
2026-01-28 14:11:34 +08:00
|
|
|
versioned := VersionedTransaction{
|
|
|
|
|
Signatures: make([]solana.Signature, 0, 10),
|
|
|
|
|
StaticAccountKeys: make([]solana.PublicKey, 0, 256),
|
|
|
|
|
Instructions: make([]Instructions, 0, 16),
|
|
|
|
|
AddressTableLookups: make([]AddressTableLookup, 0, 10),
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-07 21:15:54 +08:00
|
|
|
for _, rawSig := range protoTx.Signatures {
|
|
|
|
|
versioned.Signatures = append(versioned.Signatures, solana.SignatureFromBytes(rawSig))
|
2025-12-26 10:57:37 +08:00
|
|
|
}
|
2026-01-28 14:11:34 +08:00
|
|
|
|
|
|
|
|
versioned.StaticAccountKeys = versioned.StaticAccountKeys[:0]
|
2026-01-07 21:15:54 +08:00
|
|
|
for _, key := range msg.AccountKeys {
|
2026-01-28 14:11:34 +08:00
|
|
|
versioned.StaticAccountKeys = append(versioned.StaticAccountKeys, solana.PublicKeyFromBytes(key))
|
2025-12-26 10:57:37 +08:00
|
|
|
}
|
2026-01-28 14:11:34 +08:00
|
|
|
|
|
|
|
|
versioned.Instructions = versioned.Instructions[:0]
|
2026-01-07 21:15:54 +08:00
|
|
|
for _, instr := range msg.Instructions {
|
2026-01-28 14:11:34 +08:00
|
|
|
accounts := make([]uint8, 0, 16)
|
2026-01-07 21:15:54 +08:00
|
|
|
accounts = append(accounts, instr.Accounts...)
|
2026-01-28 14:11:34 +08:00
|
|
|
versioned.Instructions = append(versioned.Instructions, Instructions{
|
|
|
|
|
ProgramIDIndex: uint8(instr.ProgramIdIndex),
|
|
|
|
|
Accounts: accounts,
|
|
|
|
|
Data: instr.Data,
|
|
|
|
|
})
|
2025-12-26 10:57:37 +08:00
|
|
|
}
|
|
|
|
|
|
2026-01-28 14:11:34 +08:00
|
|
|
versioned.AddressTableLookups = versioned.AddressTableLookups[:0]
|
2026-01-07 21:15:54 +08:00
|
|
|
for _, lookup := range msg.AddressTableLookups {
|
2026-01-28 14:11:34 +08:00
|
|
|
writable := make([]uint8, 0, 16)
|
2026-01-07 21:15:54 +08:00
|
|
|
writable = append(writable, lookup.WritableIndexes...)
|
2026-01-28 14:11:34 +08:00
|
|
|
readonly := make([]uint8, 0, 16)
|
2026-01-07 21:15:54 +08:00
|
|
|
readonly = append(readonly, lookup.ReadonlyIndexes...)
|
2026-01-28 14:11:34 +08:00
|
|
|
versioned.AddressTableLookups = append(versioned.AddressTableLookups, AddressTableLookup{
|
2025-12-26 10:57:37 +08:00
|
|
|
AccountKey: solana.PublicKeyFromBytes(lookup.AccountKey),
|
|
|
|
|
WritableIndexes: writable,
|
|
|
|
|
ReadonlyIndexes: readonly,
|
2026-01-07 21:15:54 +08:00
|
|
|
})
|
2025-12-26 10:57:37 +08:00
|
|
|
}
|
|
|
|
|
|
2026-01-07 21:15:54 +08:00
|
|
|
versioned.Block = update.GetSlot()
|
|
|
|
|
return versioned, nil
|
2025-12-26 10:57:37 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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))
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-22 14:32:45 +08:00
|
|
|
func findAssociatedTokenAddressWithTokenProgram(wallet, mint, tokenProgram solana.PublicKey) (solana.PublicKey, uint8, error) {
|
|
|
|
|
return solana.FindProgramAddress([][]byte{
|
|
|
|
|
wallet[:],
|
|
|
|
|
tokenProgram[:],
|
|
|
|
|
mint[:],
|
|
|
|
|
}, solana.SPLAssociatedTokenAccountProgramID)
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-30 11:03:11 +08:00
|
|
|
func matchMethod(data []byte, methods []byte) bool {
|
|
|
|
|
if len(data) < len(methods) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return bytes.Equal(data[0:len(methods)], methods)
|
|
|
|
|
}
|