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 = 399015152 var data = NewBlockData(decimal.NewFromFloat(100.0)) client := rpc.New("https://staked.helius-rpc.com?api-key=") var rewards = false var version uint64 = 0 blocks, err := client.GetBlockWithOpts(context.Background(), slot, &rpc.GetBlockOpts{ TransactionDetails: rpc.TransactionDetailsFull, Rewards: &rewards, Commitment: rpc.CommitmentFinalized, Encoding: solana.EncodingBase64, MaxSupportedTransactionVersion: &version, }) if err != nil { slot++ fmt.Println("get block error:", err) return } solana_parser.EnableAllParsers() var txs []*solana_parser.Tx for i, tx := range blocks.Transactions { var blockTime uint64 if blocks.BlockTime != nil { blockTime = uint64(*blocks.BlockTime) } rawTx, err := solana_parser.FromRpcTransactionWithMeta(tx, &blockTime, slot, int64(i)) if err != nil { fmt.Println("from rpc tx error:", i, err) break } if rawTx.Meta.Err != nil { continue } parsedTx, err := solana_parser.ParseRawTx(rawTx) if err != nil { fmt.Println("parse tx error:", i, rawTx.TxHash(), err) break } 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) } } if result.GetTxHash() == "4h3yrAfMfHYHgf2DBnaecRjuSw4UTirySej65PSapPPPASvBADo143NhptQyVQdiCKypoSs2tzh3EhYxcgxVNLHD" { fmt.Println("xxx") } } 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, "") if err != nil { return 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.CheckPlatformOnSig(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, ) }