2025-11-20 17:56:45 +08:00
|
|
|
package pump_parser
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"errors"
|
2026-01-02 10:42:44 +08:00
|
|
|
"log"
|
2026-02-09 14:46:19 +08:00
|
|
|
"slices"
|
2025-11-20 17:56:45 +08:00
|
|
|
|
|
|
|
|
"github.com/gagliardetto/solana-go"
|
|
|
|
|
"github.com/shopspring/decimal"
|
|
|
|
|
)
|
|
|
|
|
|
2026-01-08 12:00:59 +08:00
|
|
|
var defaultSwapPrograms = map[solana.PublicKey]swapParser{
|
|
|
|
|
pumpAmmProgram: pumpAmmParser,
|
|
|
|
|
pumpProgram: pumpParser,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var swapPrograms = cloneSwapPrograms(defaultSwapPrograms)
|
|
|
|
|
|
|
|
|
|
type ParserOption func(*parserConfig)
|
|
|
|
|
|
|
|
|
|
type parserConfig struct {
|
|
|
|
|
enableMeteoraDlmm bool
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 14:46:19 +08:00
|
|
|
func EnableAllParsers() {
|
|
|
|
|
programs := cloneSwapPrograms(defaultSwapPrograms)
|
|
|
|
|
programs[meteoraDlmmProgram] = metaoradlmmParser
|
|
|
|
|
programs[metaoraPoolProgramID] = metaoraPoolParser
|
|
|
|
|
programs[metaoraBcProgramID] = metaoraBcParser
|
|
|
|
|
programs[meteoraDammV2Program] = metaoraDammParser
|
|
|
|
|
programs[orcaProgramID] = orcaWhirPoolParser
|
|
|
|
|
programs[raydiumV4Program] = raydiumV4Parser
|
|
|
|
|
programs[raydiumClmmProgramID] = raydiumClmmParser
|
|
|
|
|
programs[raydiumCPmmProgramID] = raydiumCPmmParser
|
|
|
|
|
programs[raydiumLaunchLabProgramID] = raydiumLaunchLabParser
|
|
|
|
|
swapPrograms = programs
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-08 12:00:59 +08:00
|
|
|
func InitParser(opts ...ParserOption) {
|
|
|
|
|
cfg := parserConfig{}
|
|
|
|
|
for _, opt := range opts {
|
|
|
|
|
opt(&cfg)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
programs := cloneSwapPrograms(defaultSwapPrograms)
|
|
|
|
|
if cfg.enableMeteoraDlmm {
|
|
|
|
|
programs[meteoraDlmmProgram] = metaoradlmmParser
|
|
|
|
|
}
|
|
|
|
|
swapPrograms = programs
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func WithMeteoraDlmm() ParserOption {
|
|
|
|
|
return func(cfg *parserConfig) {
|
|
|
|
|
cfg.enableMeteoraDlmm = true
|
|
|
|
|
}
|
2025-11-20 17:56:45 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var actionPrograms = map[solana.PublicKey]actionParser{
|
|
|
|
|
systemProgram: systemParser,
|
|
|
|
|
budgGetProgram: budgetParser,
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-27 16:39:01 +08:00
|
|
|
func ParseRawTx(rawTx *RawTx) (*Tx, error) {
|
|
|
|
|
tx := &Tx{
|
|
|
|
|
rawTx: rawTx,
|
|
|
|
|
}
|
|
|
|
|
err := tx.Parser()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return tx, nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-21 12:01:44 +08:00
|
|
|
func (tx *Tx) Parser() error {
|
|
|
|
|
if tx.rawTx == nil {
|
|
|
|
|
return errors.New("rawTx is nil")
|
2025-11-20 17:56:45 +08:00
|
|
|
}
|
2025-11-21 12:01:44 +08:00
|
|
|
accountList := tx.rawTx.getAccountList()
|
2026-02-02 14:13:00 +08:00
|
|
|
if tx.rawTx.Meta.Err == nil {
|
|
|
|
|
for _, acc := range tx.rawTx.Transaction.Message.Instructions {
|
|
|
|
|
if accountList[acc.ProgramIDIndex] == solana.VoteProgramID {
|
|
|
|
|
tx.Vote = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-11-21 12:01:44 +08:00
|
|
|
|
|
|
|
|
tx.TxHash = (*[64]byte)((tx.rawTx.Transaction.Signatures[0][:]))
|
|
|
|
|
tx.Signer = tx.rawTx.GetSigner()
|
|
|
|
|
tx.Block = tx.rawTx.Slot
|
|
|
|
|
tx.BlockIndex = uint64(tx.rawTx.IndexWithinBlock)
|
|
|
|
|
tx.BlockAt = tx.rawTx.BlockTime
|
2026-01-05 11:46:54 +08:00
|
|
|
tx.CuFee = decimal.NewFromUint64(tx.rawTx.Meta.Fee)
|
2025-11-21 12:01:44 +08:00
|
|
|
|
|
|
|
|
tx.BeforeSolBalance = decimal.NewFromUint64(tx.rawTx.Meta.PreBalances[0]).Div(decimal.NewFromInt(1e9))
|
|
|
|
|
tx.AfterSOLBalance = decimal.NewFromUint64(tx.rawTx.Meta.PostBalances[0]).Div(decimal.NewFromInt(1e9))
|
|
|
|
|
|
|
|
|
|
tx.Token = make(map[solana.PublicKey]TokenMeta)
|
|
|
|
|
|
2025-11-20 17:56:45 +08:00
|
|
|
var innersMap = make(map[int]InnerInstructions)
|
2025-11-21 12:01:44 +08:00
|
|
|
for _, inner := range tx.rawTx.Meta.InnerInstructions {
|
2025-11-20 17:56:45 +08:00
|
|
|
innersMap[inner.Index] = inner
|
|
|
|
|
}
|
2026-02-09 14:46:19 +08:00
|
|
|
txIndex := 0
|
2025-11-21 12:01:44 +08:00
|
|
|
for i, instr := range tx.rawTx.Transaction.Message.Instructions {
|
2026-02-09 14:46:19 +08:00
|
|
|
txIndex += 1
|
|
|
|
|
if i > 0 {
|
|
|
|
|
txIndex += len(innersMap[i-1].Instructions)
|
|
|
|
|
}
|
2025-11-20 17:56:45 +08:00
|
|
|
programAccount := accountList[instr.ProgramIDIndex]
|
|
|
|
|
if p, exists := swapPrograms[programAccount]; exists {
|
2025-11-21 12:01:44 +08:00
|
|
|
swaps, _, err := p(tx, instr, innersMap[i], [2]uint{uint(i), uint(0)})
|
2025-11-20 17:56:45 +08:00
|
|
|
if err != nil {
|
|
|
|
|
if errors.Is(err, InstructionIgnoredError) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2025-11-21 12:01:44 +08:00
|
|
|
return err
|
2025-11-20 17:56:45 +08:00
|
|
|
}
|
2026-02-09 14:46:19 +08:00
|
|
|
for k, swap := range swaps {
|
|
|
|
|
swap.TxIndex = txIndex + k
|
|
|
|
|
if !swap.User.IsOnCurve() {
|
|
|
|
|
swap.AfterSOLBalance = tx.AfterSOLBalance
|
|
|
|
|
swap.User = tx.rawTx.accountList[0]
|
|
|
|
|
} else {
|
|
|
|
|
userIdx := slices.Index(tx.rawTx.accountList, swap.User)
|
|
|
|
|
if userIdx >= 0 {
|
|
|
|
|
swap.AfterSOLBalance = decimal.NewFromUint64(tx.rawTx.Meta.PostBalances[userIdx]).Div(decimal.NewFromInt(1e9))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
tx.Swaps = append(tx.Swaps, swap)
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-20 17:56:45 +08:00
|
|
|
} else if p, exists := actionPrograms[programAccount]; exists {
|
2025-11-21 12:01:44 +08:00
|
|
|
_, err := p(tx, instr, innersMap[i], [2]uint{uint(i), uint(0)})
|
2025-11-20 17:56:45 +08:00
|
|
|
if err != nil {
|
|
|
|
|
if errors.Is(err, InstructionIgnoredError) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2025-11-21 12:01:44 +08:00
|
|
|
return err
|
2025-11-20 17:56:45 +08:00
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
ii := i
|
|
|
|
|
// unknown program, parser inner instructions
|
|
|
|
|
innerLength := len(innersMap[i].Instructions)
|
|
|
|
|
for j := 1; j <= innerLength; {
|
2026-01-02 10:42:44 +08:00
|
|
|
if j <= 0 || j > innerLength {
|
|
|
|
|
log.Printf("inner instruction index is out if range, block: %d, tx: %s, outerIndex: %d, innerIndex: %d", tx.Block, tx.GetTxHash(), ii, j)
|
|
|
|
|
break
|
|
|
|
|
}
|
2025-11-20 17:56:45 +08:00
|
|
|
innerInstr := innersMap[i].Instructions[j-1]
|
|
|
|
|
innerProgramAccount := accountList[innerInstr.ProgramIDIndex]
|
|
|
|
|
|
|
|
|
|
if p, exists := swapPrograms[innerProgramAccount]; exists {
|
2025-11-21 12:01:44 +08:00
|
|
|
swaps, offset, err := p(tx, innerInstr, innersMap[i], [2]uint{uint(i), uint(j)})
|
2025-11-20 17:56:45 +08:00
|
|
|
if err != nil {
|
|
|
|
|
if errors.Is(err, InstructionIgnoredError) {
|
|
|
|
|
j = int(offset[1])
|
|
|
|
|
continue
|
|
|
|
|
}
|
2025-11-21 12:01:44 +08:00
|
|
|
return err
|
2025-11-20 17:56:45 +08:00
|
|
|
}
|
2026-02-09 14:46:19 +08:00
|
|
|
for k, swap := range swaps {
|
|
|
|
|
swap.TxIndex = txIndex + k + j
|
|
|
|
|
// identify okxDexRoutersV2 and okxAggregatorV2 is user
|
|
|
|
|
//if !swap.User.IsOnCurve() && (swap.EntryContract.Equals(okxDexRoutersV2) || swap.EntryContract.Equals(okxAggregatorV2)) {
|
|
|
|
|
// swap.User = tx.rawTx.accountList[0]
|
|
|
|
|
//}
|
|
|
|
|
if !swap.User.IsOnCurve() {
|
|
|
|
|
swap.AfterSOLBalance = tx.AfterSOLBalance
|
|
|
|
|
swap.User = tx.rawTx.accountList[0]
|
|
|
|
|
} else {
|
|
|
|
|
userIdx := slices.Index(tx.rawTx.accountList, swap.User)
|
|
|
|
|
if userIdx >= 0 {
|
|
|
|
|
swap.AfterSOLBalance = decimal.NewFromUint64(tx.rawTx.Meta.PostBalances[userIdx]).Div(decimal.NewFromInt(1e9))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
tx.Swaps = append(tx.Swaps, swap)
|
|
|
|
|
}
|
|
|
|
|
// tx.Swaps = append(tx.Swaps, swaps...)
|
2025-11-20 17:56:45 +08:00
|
|
|
j = int(offset[1])
|
|
|
|
|
ii = int(offset[0])
|
|
|
|
|
} else if p, exists := actionPrograms[innerProgramAccount]; exists {
|
2025-11-21 12:01:44 +08:00
|
|
|
offset, err := p(tx, innerInstr, innersMap[i], [2]uint{uint(i), uint(j)})
|
2025-11-20 17:56:45 +08:00
|
|
|
if err != nil {
|
|
|
|
|
if errors.Is(err, InstructionIgnoredError) {
|
|
|
|
|
j = int(offset[1])
|
|
|
|
|
continue
|
|
|
|
|
}
|
2025-11-21 12:01:44 +08:00
|
|
|
return err
|
2025-11-20 17:56:45 +08:00
|
|
|
}
|
|
|
|
|
j = int(offset[1])
|
|
|
|
|
ii = int(offset[0])
|
|
|
|
|
} else {
|
|
|
|
|
j++
|
|
|
|
|
}
|
|
|
|
|
if j > innerLength || ii > i {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-09 14:46:19 +08:00
|
|
|
// update swaps same program+pair with last reserve balance
|
|
|
|
|
if len(tx.Swaps) > 1 {
|
|
|
|
|
pairKey := func(s Swap) solana.PublicKey {
|
|
|
|
|
// Match pair selection used by downstream consumers.
|
|
|
|
|
if s.Program == SolProgramPump {
|
|
|
|
|
return s.BaseMint
|
|
|
|
|
}
|
|
|
|
|
return s.Pool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
lastReserve := make(map[solana.PublicKey]reserveSnapshot, len(tx.Swaps))
|
|
|
|
|
for _, swap := range tx.Swaps {
|
|
|
|
|
lastReserve[pairKey(swap)] = reserveSnapshot{
|
|
|
|
|
baseMint: swap.BaseMint,
|
|
|
|
|
quoteMint: swap.QuoteMint,
|
|
|
|
|
baseReserve: swap.BaseReserve,
|
|
|
|
|
quoteReserve: swap.QuoteReserve,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for i := range tx.Swaps {
|
|
|
|
|
key := pairKey(tx.Swaps[i])
|
|
|
|
|
if v, ok := lastReserve[key]; ok {
|
|
|
|
|
if tx.Swaps[i].BaseMint == v.baseMint && tx.Swaps[i].QuoteMint == v.quoteMint {
|
|
|
|
|
tx.Swaps[i].BaseReserve = v.baseReserve
|
|
|
|
|
tx.Swaps[i].QuoteReserve = v.quoteReserve
|
|
|
|
|
} else if tx.Swaps[i].BaseMint == v.quoteMint && tx.Swaps[i].QuoteMint == v.baseMint {
|
|
|
|
|
tx.Swaps[i].BaseReserve = v.quoteReserve
|
|
|
|
|
tx.Swaps[i].QuoteReserve = v.baseReserve
|
|
|
|
|
}
|
|
|
|
|
//else {
|
|
|
|
|
// tx.Swaps[i].BaseReserve = v.baseReserve
|
|
|
|
|
// tx.Swaps[i].QuoteReserve = v.quoteReserve
|
|
|
|
|
//}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-11-20 17:56:45 +08:00
|
|
|
|
2025-11-21 12:01:44 +08:00
|
|
|
return nil
|
2025-11-20 17:56:45 +08:00
|
|
|
}
|
2026-01-08 12:00:59 +08:00
|
|
|
|
2026-02-09 14:46:19 +08:00
|
|
|
type reserveSnapshot struct {
|
|
|
|
|
baseMint solana.PublicKey
|
|
|
|
|
quoteMint solana.PublicKey
|
|
|
|
|
baseReserve decimal.Decimal
|
|
|
|
|
quoteReserve decimal.Decimal
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-08 12:00:59 +08:00
|
|
|
func cloneSwapPrograms(src map[solana.PublicKey]swapParser) map[solana.PublicKey]swapParser {
|
|
|
|
|
dst := make(map[solana.PublicKey]swapParser, len(src))
|
|
|
|
|
for k, v := range src {
|
|
|
|
|
dst[k] = v
|
|
|
|
|
}
|
|
|
|
|
return dst
|
|
|
|
|
}
|