From a765fafddd19ea6f33567fde4bf03a5af9001e0e Mon Sep 17 00:00:00 2001 From: thloyi Date: Mon, 20 Apr 2026 16:26:55 +0800 Subject: [PATCH] fix pump parser --- internal/test2/test.go | 778 +---------------------------------------- pump.go | 49 ++- pump_test.go | 24 ++ tx_binary.go | 5 +- 4 files changed, 73 insertions(+), 783 deletions(-) diff --git a/internal/test2/test.go b/internal/test2/test.go index ccdd395..4dd70ef 100644 --- a/internal/test2/test.go +++ b/internal/test2/test.go @@ -2,29 +2,18 @@ package main import ( "context" - "errors" "fmt" - "log" - "log/slog" - "strings" - "time" "github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go/rpc" - "github.com/jackc/pgtype" - "github.com/shopspring/decimal" solana_parser "github.com/thloyi/pump-parser" - "gorm.io/driver/postgres" - "gorm.io/gorm" - "gorm.io/gorm/logger" ) var () func main() { - var slot uint64 = 403021435 - var data = NewBlockData(decimal.NewFromFloat(100.0)) + var slot uint64 = 414432104 client := rpc.New("https://staked.helius-rpc.com?api-key=5adcf1f9-5719-43d1-bf3f-c2d4e1e5f94d") var rewards = false var version uint64 = 0 @@ -42,7 +31,7 @@ func main() { } solana_parser.EnableAllParsers() - var txs []*solana_parser.Tx + var txs []solana_parser.Tx for i, tx := range blocks.Transactions { var blockTime uint64 if blocks.BlockTime != nil { @@ -61,766 +50,11 @@ func main() { fmt.Println("parse tx error:", i, rawTx.TxHash(), err) break } - txs = append(txs, parsedTx) + txs = append(txs, *parsedTx) } - for _, result := range txs { - swapsLen := len(result.Swaps) - for i := 0; i < swapsLen; i++ { - action := result.Swaps[i] - var actions []solana_parser.Swap = make([]solana_parser.Swap, 0, 2) - actions = append(actions, action) - if i+1 < swapsLen { - nextAction := result.Swaps[i+1] - if action.Event == "buy" && nextAction.Event == "complete" && - action.Program == solana_parser.SolProgramPump && - nextAction.Program == solana_parser.SolProgramPump && - action.BaseMint == nextAction.BaseMint { - actions = append(actions, nextAction) - i++ - } - if action.Event == "migrate" && nextAction.Event == "create" && - action.Program == solana_parser.SolProgramPump && - nextAction.Program == solana_parser.SolProgramPumpAMM && - action.BaseMint == nextAction.BaseMint { - actions = append(actions, nextAction) - i++ - } - } - if err = HandleAction(context.Background(), result, actions, data); err != nil { - //h.logger.Errorf("handle action error: %s - %v", result.RawTx.Transaction.Signatures[0].String(), err) - fmt.Println("parse action error:", "tx", result.GetTxHash(), "i", i, "err", err) - } - } - - } - fmt.Println("slot", slot, "tx count: ", len(data.Txs)) - -} - -var ( - meteoraDammV2Program = solana.MustPublicKeyFromBase58("cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG") - raydiumCPmmProgramID = solana.MustPublicKeyFromBase58("CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C") -) - -func HandleAction(ctx context.Context, tx *solana_parser.Tx, swaps []solana_parser.Swap, data *BlockData) error { - swapLen := len(swaps) - if len(swaps) == 0 { - return nil - } - if swaps[0].BaseMint != solana_parser.WSOL && swaps[0].QuoteMint != solana_parser.WSOL && !swaps[0].QuoteMint.IsZero() { - return nil - } - - if len(swaps) == 0 { - return nil - } - event := swaps[0].Event - swap := swaps[0] - action := SwapGetter{swap} - switch event { - case "buy", "sell": - - data.AppendTx(action.GetTx(tx, uint64(swap.TxIndex), data.Price)) - if swap.Program == solana_parser.SolProgramPump { - if swapLen == 2 && swaps[1].Event == "complete" { - t := pgtype.Timestamptz{} - t.Set(time.Unix(tx.BlockAt, 0)) - data.AppendAction(Action{ - Maker: swaps[1].User.String(), - Token: swaps[1].BaseMint.String(), - Pair: swaps[1].Pool.String(), - Action: "pump-migrate", - Block: tx.Block, - BlockAt: t, - TxHash: tx.GetTxHash(), - }) - } - } - return data.SetPair(action, tx.Block, "") - - case "create": - pair, err := action.GetPair(tx.Block, "") - if err != nil { - return err - } - data.AppendTx(action.GetTx(tx, uint64(swap.TxIndex), data.Price)) - data.Pairs[pair.Address] = *pair - case "add_liquidity", "remove_liquidity", "deposit", "withdraw", "add", "remove": - liquidityTx, err := action.GetLiquidityTx(tx, uint64(swap.TxIndex)) - if liquidityTx == nil { - return err - } - data.AppendTx(*liquidityTx) - return data.SetPair(action, tx.Block, "") - } - - if event != "migrate" { - return nil - } - if swap.Program == solana_parser.SolProgramPump { - t := pgtype.Timestamptz{} - t.Set(time.Unix(tx.BlockAt, 0)) - if swapLen == 2 && swaps[1].Event == "create" && swaps[1].Program == solana_parser.SolProgramPumpAMM && swaps[1].BaseMint == swap.BaseMint { - tokenMint := swap.BaseMint.String() - data.AppendAction(Action{ - Maker: swap.User.String(), - Token: tokenMint, - Pair: swaps[1].Pool.String(), - Action: "on-pumpswap", - Block: tx.Block, - BlockAt: t, - TxHash: tx.GetTxHash(), - }) - data.NewRaydium = append(data.NewRaydium, tokenMint) - } - } else if swap.Program == solana_parser.SolProgramRaydiumLaunchLab || swap.Program == solana_parser.SolProgramRaydiumLaunchLabBonk { - t := pgtype.Timestamptz{} - t.Set(time.Unix(tx.BlockAt, 0)) - var actionType string - if action.MigrateTopProgram == raydiumCPmmProgramID { - actionType = "on-raydium-cpmm" - } else { - actionType = "on-raydium-amm" - } - data.AppendAction(Action{ - Maker: action.User.String(), - Token: action.BaseMint.String(), - Pair: action.MigrateToPool.String(), - Action: actionType, - Block: tx.Block, - BlockAt: t, - TxHash: tx.GetTxHash(), - }) - } else if swap.Program == solana_parser.SolProgramMeteoraBondingCurve { - t := pgtype.Timestamptz{} - t.Set(time.Unix(tx.BlockAt, 0)) - var actionType string - if swap.MigrateTopProgram == meteoraDammV2Program { - actionType = "on-meteora-amm-v2" - } else { - actionType = "on-meteora-amm-v1" - } - data.AppendAction(Action{ - Maker: action.User.String(), - Token: action.BaseMint.String(), - Pair: action.MigrateToPool.String(), - Action: actionType, - Block: uint64(tx.Block), - BlockAt: t, - TxHash: tx.GetTxHash(), - }) - } - - return nil -} - -type Pair struct { - Id string `gorm:"column:id;primaryKey;default:uuid_generate_v4()"` - Address string - Name string - Token0 string - Token1 string - LpToken string - ChainId int64 - Reserve0 decimal.Decimal - Reserve1 decimal.Decimal - Block uint64 - BlockAt *pgtype.Timestamptz `gorm:"column:block_at;default:NULL" json:"block_at,omitempty"` - CreatedAt *pgtype.Timestamptz `gorm:"autoCreateTime" json:"created_at,omitempty"` - SortId uint64 - Program string - - IsCreate bool `gorm:"-"` - //TokenObj *Token `gorm:"-" json:"token_obj,omitempty"` - UpdateSlot uint64 `gorm:"-"` - InDB bool `gorm:"-"` -} - -type Tx struct { - Id pgtype.UUID `gorm:"column:id;primaryKey;default:uuid_generate_v4()" json:"-"` - PairAddress string `json:"pair_address"` - Maker string `json:"maker"` - Token0Address string `json:"token0_address"` - Token1Address string `json:"token1_address"` - Token0Amount decimal.Decimal `json:"token0Amount" gorm:"column:token0_amount;type:numeric"` - Token1Amount decimal.Decimal `json:"token1Amount" gorm:"column:token1_amount;type:numeric"` - PriceUsd decimal.Decimal `json:"price_usd" gorm:"column:price_usd;type:numeric"` - AmountUsd decimal.Decimal `json:"amount_usd" gorm:"column:amount_usd;type:numeric"` - Block uint64 `json:"block"` - BlockIndex uint64 `json:"index"` - Event string `json:"event"` - TxHash string `json:"tx_hash"` - TxIndex uint64 `json:"topic_index"` - Program string `json:"program"` - BlockAt pgtype.Timestamptz `gorm:"column:block_at;default:NULL" json:"block_at"` - CreatedAt *pgtype.Timestamptz `gorm:"autoCreateTime" json:"-"` - TotalSupply string `gorm:"total_supply"` - AfterReserve0 string `gorm:"after_reserve0"` - AfterReserve1 string `gorm:"after_reserve1"` - PositionChange int64 `gorm:"position_change"` - Platform string `gorm:"column:tx_platform;type:platform;default:'none'" json:"tx_platform"` - PlatformFee decimal.Decimal `gorm:"-" json:"-"` // TODO: save to db - CUPrice decimal.Decimal `gorm:"column:tx_cu_price;type:numeric" json:"tx_cu_price"` - MevAgent string `gorm:"column:tx_mev_agent;type:mev_agent;default:'none'" json:"tx_mev_agent"` - MevAgentFee decimal.Decimal `gorm:"column:tx_mev_agent_fee;type:numeric" json:"tx_mev_agent_fee"` - AfterSOLBalance decimal.Decimal `gorm:"column:after_sol_balance;type:numeric" json:"after_sol_balance"` - EntryContract string `gorm:"column:tx_entry_contract;type:entry_contract;default:'none'" json:"tx_entry_contract"` -} - -type Action struct { - Id pgtype.UUID `gorm:"column:id;primaryKey;default:uuid_generate_v4()" json:"-"` - Maker string `json:"maker"` - Token string `json:"token"` - Pair string `json:"pair"` - Action string `json:"action"` - Block uint64 `json:"block"` - BlockAt pgtype.Timestamptz `json:"block_at"` - TxHash string `json:"tx_hash"` - CreatedAt *pgtype.Timestamptz `gorm:"autoCreateTime" json:"-"` -} - -type BlockData struct { - Pairs map[string]Pair - Txs []Tx - Actions []Action - Price decimal.Decimal - NewRaydium []string -} - -func NewBlockData(price decimal.Decimal) *BlockData { - return &BlockData{ - Pairs: make(map[string]Pair), - Txs: make([]Tx, 0), - Actions: make([]Action, 0), - Price: price, - NewRaydium: make([]string, 0), - } -} - -func (bd *BlockData) AppendTx(tx Tx) { - bd.Txs = append(bd.Txs, tx) -} - -func (bd *BlockData) AppendAction(action Action) { - bd.Actions = append(bd.Actions, action) -} - -func (bd *BlockData) SetPair(action SwapGetter, block uint64, _ string) error { - pair, err := action.GetPair(block, "") + _, err = solana_parser.EncodeTxsBinary(txs) if err != nil { - return err + fmt.Println("EncodeTxsBinary err", err) } - bd.Pairs[pair.Address] = *pair - return nil -} - -type SwapGetter struct { - solana_parser.Swap -} - -const ( - PositionChangeNone = int64(iota) - PositionChangeNewBuy - PositionChangeBuyMore - PositionChangeSellPart - PositionChangeSellAll -) - -func (spg SwapGetter) GetLiquidityTx(tx *solana_parser.Tx, index uint64) (*Tx, error) { - if spg.BaseMint != solana.WrappedSol && spg.QuoteMint != solana.WrappedSol { - return nil, nil - } - - var ( - token0 string - amount0 decimal.Decimal - amount1 decimal.Decimal - pool0 decimal.Decimal - pool1 decimal.Decimal - - event string - ) - - if spg.BaseMint == solana.WrappedSol { - amount0 = spg.QuoteAmount.Div(decimal.New(1, int32(spg.QuoteMintDecimals))) - amount1 = spg.BaseAmount.Div(decimal.New(1, int32(spg.BaseMintDecimals))) - token0 = spg.QuoteMint.String() - pool0 = spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals))) - pool1 = spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals))) - } else { - amount0 = spg.BaseAmount.Div(decimal.New(1, int32(spg.BaseMintDecimals))) - amount1 = spg.QuoteAmount.Div(decimal.New(1, int32(spg.QuoteMintDecimals))) - token0 = spg.BaseMint.String() - pool0 = spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals))) - pool1 = spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals))) - } - if spg.Event == "deposit" || spg.Event == "add" || spg.Event == "add_liquidity" || spg.Event == "add_liquidity_one_side" { - event = "add" - } else if spg.Event == "withdraw" || spg.Event == "remove" || spg.Event == "remove_liquidity" || spg.Event == "remove_liquidity_one_side" { - event = "remove" - } - if event == "" { - return nil, nil - } - - mevName, mevFee := tx.CheckMevAgent() - platformName, platformFee := tx.CheckPlatform(spg.Swap) - - pairString := "" - if spg.Program == solana_parser.SolProgramPump { - pairString = spg.BaseMint.String() - } else { - pairString = spg.Pool.String() - } - t := pgtype.Timestamptz{} - _ = t.Set(time.Unix(tx.BlockAt, 0)) - return &Tx{ - PairAddress: pairString, - Maker: spg.User.String(), - Token0Address: token0, - Token1Address: "So11111111111111111111111111111111111111112", - Token0Amount: amount0, - Token1Amount: amount1, - Block: tx.Block, - BlockIndex: tx.BlockIndex, - Event: event, - TxHash: tx.GetTxHash(), - TxIndex: index, - BlockAt: t, - Program: spg.Program, - AfterReserve0: pool0.String(), - AfterReserve1: pool1.String(), - Platform: platformName, - PlatformFee: platformFee, - CUPrice: tx.CUPrice, - MevAgent: mevName, - MevAgentFee: mevFee, - AfterSOLBalance: spg.AfterSOLBalance, - EntryContract: spg.CheckEntryContract(), - }, nil -} - -func (spg SwapGetter) GetTx(tx *solana_parser.Tx, index uint64, price decimal.Decimal) Tx { - var ( - token0 string - amount0 decimal.Decimal - amount1 decimal.Decimal - pool0 decimal.Decimal - pool1 decimal.Decimal - - event string - ) - - if spg.BaseMint == solana.WrappedSol { - amount0 = spg.QuoteAmount.Div(decimal.New(1, int32(spg.QuoteMintDecimals))) - amount1 = spg.BaseAmount.Div(decimal.New(1, int32(spg.BaseMintDecimals))) - token0 = spg.QuoteMint.String() - pool0 = spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals))) - pool1 = spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals))) - if spg.Event == "buy" { - event = "sell" - } else if spg.Event == "sell" { - event = "buy" - } - } else { - amount0 = spg.BaseAmount.Div(decimal.New(1, int32(spg.BaseMintDecimals))) - amount1 = spg.QuoteAmount.Div(decimal.New(1, int32(spg.QuoteMintDecimals))) - token0 = spg.BaseMint.String() - pool0 = spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals))) - pool1 = spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals))) - event = spg.Event - } - - priceUsd := decimal.Zero - if amount0.GreaterThan(priceUsd) { - priceUsd = amount1.Div(amount0).Mul(price) - } - pc := PositionChangeNone - if event == "buy" { - pc = PositionChangeNewBuy - if spg.BaseMint == solana.WrappedSol { - if spg.UserQuoteBalance.GreaterThan(spg.QuoteAmount) { - pc = PositionChangeBuyMore - } - } else { - if spg.UserBaseBalance.GreaterThan(spg.BaseAmount) { - pc = PositionChangeBuyMore - } - } - } else if event == "sell" { - pc = PositionChangeSellPart - if spg.BaseMint == solana.WrappedSol { - if spg.UserQuoteBalance.Div(decimal.New(1, int32(spg.QuoteMintDecimals))).LessThan(decimal.NewFromFloat(0.01)) { - pc = PositionChangeSellAll - } - } else { - if spg.UserBaseBalance.Div(decimal.New(1, int32(spg.BaseMintDecimals))).LessThan(decimal.NewFromFloat(0.01)) { - pc = PositionChangeSellAll - } - } - } - - mevName, mevFee := tx.CheckMevAgent() - platformName, platformFee := tx.CheckPlatform(spg.Swap) - - if mevName == "" { - mevName = "none" - } - if mevName == "unknown" { - mevName = "none" - mevFee = decimal.Zero - } - pairString := "" - if spg.Program == solana_parser.SolProgramPump { - pairString = spg.BaseMint.String() - } else { - pairString = spg.Pool.String() - } - t := pgtype.Timestamptz{} - _ = t.Set(time.Unix(tx.BlockAt, 0)) - - return Tx{ - PairAddress: pairString, - Maker: spg.User.String(), - Token0Address: token0, - Token1Address: "So11111111111111111111111111111111111111112", - Token0Amount: amount0, - Token1Amount: amount1, - PriceUsd: priceUsd, - AmountUsd: amount1.Mul(price), - Block: tx.Block, - BlockIndex: tx.BlockIndex, - Event: event, - TxHash: tx.GetTxHash(), - TxIndex: index, - BlockAt: t, - Program: spg.Program, - AfterReserve0: pool0.String(), - AfterReserve1: pool1.String(), - PositionChange: pc, - Platform: platformName, - PlatformFee: platformFee, - CUPrice: tx.CUPrice, - MevAgent: mevName, - MevAgentFee: mevFee, - AfterSOLBalance: spg.AfterSOLBalance, - EntryContract: spg.CheckEntryContract(), - } -} - -func (spg SwapGetter) GetPair(slot uint64, _ string) (*Pair, error) { - //pump amm - if spg.Program == solana_parser.SolProgramPump { - tokenMint := spg.BaseMint.String() - return &Pair{ - Address: tokenMint, - Token0: tokenMint, - Token1: "So11111111111111111111111111111111111111112", - ChainId: 900, - Reserve0: spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals))), - Reserve1: spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals))), - IsCreate: spg.Event == "create", - Program: spg.Program, - UpdateSlot: slot, - }, nil - } else { - var ( - token0 string - amount0 decimal.Decimal - amount1 decimal.Decimal - ) - if spg.BaseMint.IsZero() || spg.QuoteMint.IsZero() { - return nil, errors.New("base mint or quote mint is empty") - } - - if spg.BaseMint == solana.WrappedSol { - amount0 = spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals))) - amount1 = spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals))) - //decimal0 = spg.QuoteMintDecimals - token0 = spg.QuoteMint.String() - } else { - amount0 = spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals))) - amount1 = spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals))) - //decimal0 = a.BaseDecimals - token0 = spg.BaseMint.String() - } - - return &Pair{ - Address: spg.Pool.String(), - LpToken: spg.LpMint.String(), - Token0: token0, - Token1: "So11111111111111111111111111111111111111112", - ChainId: 900, - Reserve0: amount0, - Reserve1: amount1, - IsCreate: false, - Program: spg.Program, - UpdateSlot: slot, - }, nil - } -} - -func getBlockTxsFromDb(db *gorm.DB, block uint64) ([]Tx, error) { - var txs []Tx - result := db.Table("tx").Where("block = ?", block).Find(&txs) - return txs, result.Error -} - -func getBlockActionsFromDb(db *gorm.DB, block uint64) ([]Action, error) { - var txs []Action - result := db.Table("action").Where("block = ?", block).Find(&txs) - return txs, result.Error -} - -type dbLog struct { - logger *slog.Logger -} - -func (l *dbLog) Printf(format string, args ...interface{}) { - l.logger.Info(fmt.Sprintf(format, args...)) -} - -func newDbLog() *dbLog { - return &dbLog{logger: slog.Default()} -} - -func NewGorm(dsn string) *gorm.DB { - db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{ - Logger: logger.New(newDbLog(), logger.Config{ - Colorful: false, - LogLevel: logger.Warn, - SlowThreshold: time.Second * 10, - IgnoreRecordNotFoundError: true, - }), - }) - if err != nil { - panic(err) - } - - return db -} - -func compareTxs(dbTxs []Tx, dataTxs []Tx) (diff int, missing int) { - dataByHash := make(map[string][]Tx, len(dataTxs)) - for _, tx := range dataTxs { - dataByHash[fmt.Sprintf("%s-%d", tx.TxHash, tx.TxIndex)] = append(dataByHash[fmt.Sprintf("%s-%d", tx.TxHash, tx.TxIndex)], tx) - } - - for _, dbTx := range dbTxs { - candidates := dataByHash[fmt.Sprintf("%s-%d", dbTx.TxHash, dbTx.TxIndex)] - if len(candidates) == 0 { - missing++ - log.Printf("missing tx: %s", txCompareString(dbTx)) - continue - } - matched := false - for _, dataTx := range candidates { - if txEqualWithoutHash(dbTx, dataTx) { - matched = true - break - } - } - if !matched { - diff++ - log.Printf("tx diff hash=%s, program=%s, event:%s: %s, ", dbTx.TxHash, dbTx.Program, dbTx.Event, txCompareDiffString(dbTx, candidates[0])) - } - } - log.Printf("compare txs done: db=%d parsed=%d missing=%d diff=%d", len(dbTxs), len(dataTxs), missing, diff) - return diff, missing -} - -func withinOnePercentDecimal(a decimal.Decimal, b decimal.Decimal) bool { - if a.IsZero() { - return b.IsZero() - } - diff := a.Sub(b).Abs() - threshold := a.Abs().Mul(decimal.NewFromFloat(0.03)) - return diff.LessThanOrEqual(threshold) -} - -func withinOnePercentStringDecimal(a string, b string) bool { - ad, errA := decimal.NewFromString(a) - bd, errB := decimal.NewFromString(b) - if errA != nil || errB != nil { - return a == b - } - return withinOnePercentDecimal(ad, bd) -} - -func txEqualWithoutHash(a Tx, b Tx) bool { - //mevMatch := a.MevAgent == b.MevAgent || (a.MevAgent == "none" && b.MevAgent == "unknown") || (a.MevAgent == "unknown" && b.MevAgent == "none") - //mevNone := a.MevAgent == "none" || a.MevAgent == "unknown" - - return a.PairAddress == b.PairAddress && - a.Token1Address == b.Token1Address && - (a.Token0Address == "" || a.Token0Address == b.Token0Address) && - //a.Maker == b.Maker && - (a.Token0Address == "" || withinOnePercentDecimal(a.Token0Amount, b.Token0Amount)) && - withinOnePercentDecimal(a.Token1Amount, b.Token1Amount) && - a.Block == b.Block && - a.BlockIndex == b.BlockIndex && - a.Event == b.Event && - a.TxIndex == b.TxIndex && - a.Program == b.Program && - (a.Program == solana_parser.SolProgramPumpAMM || a.Program == solana_parser.SolProgramPump || a.Program == solana_parser.SolProgramMeteoraPools || (a.Event == "create") || withinOnePercentStringDecimal(a.AfterReserve0, b.AfterReserve0)) && - (a.Program == solana_parser.SolProgramPumpAMM || a.Program == solana_parser.SolProgramPump || a.Program == solana_parser.SolProgramMeteoraPools || (a.Event == "create") || withinOnePercentStringDecimal(a.AfterReserve1, b.AfterReserve1)) && - // a.PositionChange == b.PositionChange && - (a.Platform == b.Platform || (a.Platform == "photon" && b.Platform == "fake") || (a.Platform == "trojan" && b.Platform == "fake")) && - a.CUPrice.String() == b.CUPrice.String() // && - //mevMatch && - //(mevNone || a.MevAgentFee.String() == b.MevAgentFee.String()) && - //(a.Program == solana_parser.SolProgramRaydiumV4 || a.AfterSOLBalance.String() == b.AfterSOLBalance.String()) - //&& - // a.EntryContract == b.EntryContract -} - -func txCompareDiffString(a Tx, b Tx) string { - var diffs []string - if a.PairAddress != b.PairAddress { - diffs = append(diffs, fmt.Sprintf("PairAddress db=%s data=%s", a.PairAddress, b.PairAddress)) - } - //if a.Maker != b.Maker { - // diffs = append(diffs, fmt.Sprintf("Maker db=%s, data=%s", a.Maker, b.Maker)) - //} - if a.Token1Address != b.Token1Address { - diffs = append(diffs, fmt.Sprintf("Token1Address db=%s data=%s", a.Token1Address, b.Token1Address)) - } - if a.Token0Address != b.Token0Address { - diffs = append(diffs, fmt.Sprintf("Token0Address db=%s data=%s", a.Token0Address, b.Token0Address)) - } - if !withinOnePercentDecimal(a.Token0Amount, b.Token0Amount) { - diffs = append(diffs, fmt.Sprintf("Token0Amount db=%s data=%s", a.Token0Amount.String(), b.Token0Amount.String())) - } - if !withinOnePercentDecimal(a.Token1Amount, b.Token1Amount) { - diffs = append(diffs, fmt.Sprintf("Token1Amount db=%s data=%s", a.Token1Amount.String(), b.Token1Amount.String())) - } - if a.Block != b.Block { - diffs = append(diffs, fmt.Sprintf("Block db=%d data=%d", a.Block, b.Block)) - } - if a.BlockIndex != b.BlockIndex { - diffs = append(diffs, fmt.Sprintf("BlockIndex db=%d data=%d", a.BlockIndex, b.BlockIndex)) - } - if a.Event != b.Event { - diffs = append(diffs, fmt.Sprintf("Event db=%s data=%s", a.Event, b.Event)) - } - if a.TxIndex != b.TxIndex { - diffs = append(diffs, fmt.Sprintf("TxIndex db=%d data=%d", a.TxIndex, b.TxIndex)) - } - if a.Program != b.Program { - diffs = append(diffs, fmt.Sprintf("Program db=%s data=%s", a.Program, b.Program)) - } - if !withinOnePercentStringDecimal(a.AfterReserve0, b.AfterReserve0) { - diffs = append(diffs, fmt.Sprintf("AfterReserve0 db=%s data=%s", a.AfterReserve0, b.AfterReserve0)) - } - if !withinOnePercentStringDecimal(a.AfterReserve1, b.AfterReserve1) { - diffs = append(diffs, fmt.Sprintf("AfterReserve1 db=%s data=%s", a.AfterReserve1, b.AfterReserve1)) - } - //if a.PositionChange != b.PositionChange { - // diffs = append(diffs, fmt.Sprintf("PositionChange db=%d data=%d", a.PositionChange, b.PositionChange)) - //} - if a.Platform != b.Platform { - diffs = append(diffs, fmt.Sprintf("Platform db=%s data=%s", a.Platform, b.Platform)) - } - if a.CUPrice.String() != b.CUPrice.String() { - diffs = append(diffs, fmt.Sprintf("CUPrice db=%s data=%s", a.CUPrice.String(), b.CUPrice.String())) - } - //if a.MevAgent != b.MevAgent { - // diffs = append(diffs, fmt.Sprintf("MevAgent db=%s data=%s", a.MevAgent, b.MevAgent)) - //} - //if a.MevAgentFee.String() != b.MevAgentFee.String() { - // diffs = append(diffs, fmt.Sprintf("MevAgentFee db=%s data=%s", a.MevAgentFee.String(), b.MevAgentFee.String())) - //} - //if a.AfterSOLBalance.String() != b.AfterSOLBalance.String() { - // diffs = append(diffs, fmt.Sprintf("AfterSOLBalance db=%s data=%s", a.AfterSOLBalance.String(), b.AfterSOLBalance.String())) - //} - //if a.EntryContract != b.EntryContract { - // diffs = append(diffs, fmt.Sprintf("EntryContract db=%s data=%s", a.EntryContract, b.EntryContract)) - //} - return strings.Join(diffs, "; ") -} - -func compareActions(dbActions []Action, dataActions []Action) (diff, missing int) { - dataByHash := make(map[string][]Action, len(dataActions)) - for _, action := range dataActions { - dataByHash[action.TxHash] = append(dataByHash[action.TxHash], action) - } - - for _, dbAction := range dbActions { - candidates := dataByHash[dbAction.TxHash] - if len(candidates) == 0 { - missing++ - log.Printf("missing action: %s", actionCompareString(dbAction)) - continue - } - matched := false - for _, dataAction := range candidates { - if actionEqualWithoutHash(dbAction, dataAction) { - matched = true - break - } - } - if !matched { - diff++ - log.Printf("action diff hash=%s: %s", dbAction.TxHash, actionCompareDiffString(dbAction, candidates[0])) - } - } - log.Printf("compare actions done: db=%d parsed=%d missing=%d diff=%d", len(dbActions), len(dataActions), missing, diff) - return diff, missing -} - -func actionEqualWithoutHash(a Action, b Action) bool { - return a.Maker == b.Maker && - a.Token == b.Token && - a.Pair == b.Pair && - a.Action == b.Action && - a.Block == b.Block -} - -func actionCompareDiffString(a Action, b Action) string { - var diffs []string - if a.Maker != b.Maker { - diffs = append(diffs, fmt.Sprintf("Maker db=%s data=%s", a.Maker, b.Maker)) - } - if a.Token != b.Token { - diffs = append(diffs, fmt.Sprintf("Token db=%s data=%s", a.Token, b.Token)) - } - if a.Pair != b.Pair { - diffs = append(diffs, fmt.Sprintf("Pair db=%s data=%s", a.Pair, b.Pair)) - } - if a.Action != b.Action { - diffs = append(diffs, fmt.Sprintf("Action db=%s data=%s", a.Action, b.Action)) - } - if a.Block != b.Block { - diffs = append(diffs, fmt.Sprintf("Block db=%d data=%d", a.Block, b.Block)) - } - return strings.Join(diffs, "; ") -} - -func actionCompareString(action Action) string { - return fmt.Sprintf("Maker=%s Token=%s Pair=%s Action=%s Block=%d TxHash=%s", action.Maker, action.Token, action.Pair, action.Action, action.Block, action.TxHash) -} - -func txCompareString(tx Tx) string { - return fmt.Sprintf( - "tx.Program=%s TxHash=%s PairAddress=%s Token1Address=%s Token0Amount=%s Token1Amount=%s Block=%d BlockIndex=%d Event=%s TxIndex=%d AfterReserve0=%s AfterReserve1=%s PositionChange=%d Platform=%s CUPrice=%s MevAgent=%s MevAgentFee=%s AfterSOLBalance=%s EntryContract=%s", - tx.Program, - tx.TxHash, - tx.PairAddress, - tx.Token1Address, - tx.Token0Amount.String(), - tx.Token1Amount.String(), - tx.Block, - tx.BlockIndex, - tx.Event, - tx.TxIndex, - tx.AfterReserve0, - tx.AfterReserve1, - tx.PositionChange, - tx.Platform, - tx.CUPrice.String(), - tx.MevAgent, - tx.MevAgentFee.String(), - tx.AfterSOLBalance.String(), - tx.EntryContract, - ) + } diff --git a/pump.go b/pump.go index 81d687b..fa675d0 100644 --- a/pump.go +++ b/pump.go @@ -231,6 +231,19 @@ func pumpTradeAmountInfoFromArgs(args PumpTradeArgs) (swapMode SwapMode, fixedAm } } +func pumpCompleteMatchesTradeEvent(completeEvent CompleteEvent, tradeEvent PumpTradeEvent, bondingCurve solana.PublicKey) bool { + if completeEvent.Mint != tradeEvent.Mint { + return false + } + if completeEvent.User != tradeEvent.User { + return false + } + if completeEvent.BondingCurve != bondingCurve { + return false + } + return true +} + func normalizePumpQuoteSideMint(s *Swap) { if s.FixedAmountSide == SwapAmountSideQuote && s.FixedMint.IsZero() { s.FixedMint = wSolMint @@ -366,6 +379,7 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns completeEvent CompleteEvent completed bool newoffset [2]uint + tradeFound bool ) var prefixLen = offset[1] @@ -394,6 +408,9 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns } if innerInstr.ProgramIDIndex == programIndex && bytes.Equal(innerInstr.Data[:8], pumpEventDiscriminator[:]) { if bytes.Equal(innerInstr.Data[8:16], pumpTradeEventDiscriminator[8:16]) { + if tradeFound { + break + } err = agbinary.NewBorshDecoder(innerInstr.Data[16:]).Decode(&tradeEvent) if offset[1] == 0 { newoffset = [2]uint{offset[0] + 1, offset[1]} @@ -403,19 +420,31 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns if err != nil { return nil, newoffset, fmt.Errorf("pump buy event decode error: %v, offset, %d, %d", err, offset[0], offset[1]) } + expectedIsBuy := !bytes.Equal(instruction.Data[:8], pumpSellDiscriminator[:]) + if tradeEvent.IsBuy != expectedIsBuy { + tradeEvent = PumpTradeEvent{} + continue + } + tradeFound = true if !tradeEvent.IsBuy { break } } else if bytes.Equal(innerInstr.Data[8:16], pumpCompleteEventDiscriminator[:]) { + if !tradeFound { + continue + } err = agbinary.NewBorshDecoder(innerInstr.Data[16:]).Decode(&completeEvent) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("pump completeEvent event decode error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + if !pumpCompleteMatchesTradeEvent(completeEvent, tradeEvent, result.accountList[instruction.Accounts[3]]) { + break + } if offset[1] == 0 { newoffset = [2]uint{offset[0] + 1, offset[1]} } else { newoffset = [2]uint{offset[0], prefixLen + uint(innerIndex) + 1} } - if err != nil { - return nil, newoffset, fmt.Errorf("pump completeEvent event decode error: %v, offset, %d, %d", err, offset[0], offset[1]) - } completed = true break } @@ -428,6 +457,11 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns offset = [2]uint{newoffset[0], newoffset[1]} + var args PumpTradeArgs + if err := agbinary.NewBorshDecoder(instruction.Data[:]).Decode(&args); err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed tx pump buy/sell decode error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + event := "" baseTokenProgram := solana.TokenProgramID if tradeEvent.IsBuy { @@ -495,12 +529,9 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns Cashback: isCashbackCoin, }, } - var args PumpTradeArgs - if err := agbinary.NewBorshDecoder(instruction.Data[:]).Decode(&args); err == nil { - if swapMode, fixedAmount, limitAmount, ok := pumpTradeAmountInfoFromArgs(args); ok { - swaps[0].SetSwapAmountInfo(swapMode, fixedAmount, limitAmount) - normalizePumpQuoteSideMint(&swaps[0]) - } + if swapMode, fixedAmount, limitAmount, ok := pumpTradeAmountInfoFromArgs(args); ok { + swaps[0].SetSwapAmountInfo(swapMode, fixedAmount, limitAmount) + normalizePumpQuoteSideMint(&swaps[0]) } if completed { swaps = append(swaps, Swap{ diff --git a/pump_test.go b/pump_test.go index 04daf65..dc89c8e 100644 --- a/pump_test.go +++ b/pump_test.go @@ -76,3 +76,27 @@ func TestCal(t *testing.T) { fmt.Println(solana.MustPublicKeyFromBase58("BM9CcyErJcu2mjrFvUsRRrD3snGeHDDVirJLvL6EjvMN").IsOnCurve()) } + +func TestPumpCompleteMatchesTradeEvent(t *testing.T) { + mint := solana.MustPublicKeyFromBase58("8GNGkNnfBuoTP3QRnmdNzSYuuE15M8tvcNvxNsV4pump") + user := solana.MustPublicKeyFromBase58("DS95KxqUCCjwQaXhD7fhKatXbivwWDNrJdNV5ZcubGdz") + bondingCurve := solana.MustPublicKeyFromBase58("Gz5EX3X7kUDS48baijJKubQDKy3BBKpnMJQ3f3W1e9jA") + + tradeEvent := PumpTradeEvent{ + Mint: mint, + User: user, + } + completeEvent := CompleteEvent{ + Mint: mint, + User: user, + BondingCurve: bondingCurve, + } + if !pumpCompleteMatchesTradeEvent(completeEvent, tradeEvent, bondingCurve) { + t.Fatal("pumpCompleteMatchesTradeEvent() = false, want true") + } + + completeEvent.User = solana.MustPublicKeyFromBase58("3g89wLRwJ5P22fkCdPJBAP7iiYAo6yY96geQvMYj6tYm") + if pumpCompleteMatchesTradeEvent(completeEvent, tradeEvent, bondingCurve) { + t.Fatal("pumpCompleteMatchesTradeEvent() = true for mismatched user") + } +} diff --git a/tx_binary.go b/tx_binary.go index c1a9933..7adbaa5 100644 --- a/tx_binary.go +++ b/tx_binary.go @@ -12,6 +12,7 @@ import ( "strconv" "github.com/gagliardetto/solana-go" + "github.com/mr-tron/base58" "github.com/shopspring/decimal" ) @@ -186,7 +187,7 @@ func NewTxsBinary(txs []Tx) (*TxsBinary, error) { for i, tx := range txPtrs { binaryTx, err := newTxBinaryWithAddressTable(tx, addressTable, addressIndex) if err != nil { - return nil, fmt.Errorf("tx[%d]: %w", i, err) + return nil, fmt.Errorf("tx[%d], %s: %w", i, base58.Encode(tx.TxHash[:]), err) } out.Txs = append(out.Txs, *binaryTx) } @@ -478,7 +479,7 @@ func (txs *TxsBinary) MarshalBinary() ([]byte, error) { enc.writeUint32(uint32(len(txs.Txs))) for i := range txs.Txs { if err := enc.writeTxBinaryBody(&txs.Txs[i], enumTable); err != nil { - return nil, fmt.Errorf("tx[%d]: %w", i, err) + return nil, fmt.Errorf("tx[%d], %s: %w", i, base58.Encode(txs.Txs[i].TxHash[:]), err) } } return enc.bytes(), nil