package shreder import ( "bytes" "encoding/binary" "fmt" bin "github.com/gagliardetto/binary" "github.com/gagliardetto/solana-go" "github.com/shopspring/decimal" ) var ( okxDexRouteV2ProgramID = solana.MustPublicKeyFromBase58("proVF4pMXVaYqmy4NjniPh4pqKNfMmsihgd4wdkCX3u") okxDexRouteV2ProgramIDString = okxDexRouteV2ProgramID.String() okxSwapTobDisc = []byte{170, 41, 85, 177, 132, 80, 31, 53} okxSwapTobWithReceiverDisc = []byte{223, 170, 216, 234, 204, 6, 241, 25} okxSwapTocDisc = []byte{187, 201, 212, 51, 16, 155, 236, 60} okxSwapTocV2Disc = []byte{127, 214, 107, 189, 23, 90, 47, 104} ) // IDL: SwapArgs { order_id:u64, amount_in:u64, expect_amount_out:u64, slippage:u16, routes: Vec } // IDL: Route { dex: Dex(enum), weight:u16, index:u8 } type OkxV2Route struct { Dex OkxV2SwapKind Weight uint16 Index uint8 } type OkxV2SwapArgs struct { OrderID uint64 AmountIn uint64 ExpectAmountOut uint64 Slippage uint16 Routes []OkxV2Route } type OkxV2SwapKind uint8 const ( OKCV2_SplTokenSwap OkxV2SwapKind = iota OKCV2_StableSwap OKCV2_Whirlpool OKCV2_MeteoraDynamicpool OKCV2_RaydiumSwap OKCV2_RaydiumStableSwap OKCV2_RaydiumClmmSwap OKCV2_AldrinExchangeV1 OKCV2_AldrinExchangeV2 OKCV2_LifinityV1 OKCV2_LifinityV2 OKCV2_RaydiumClmmSwapV2 OKCV2_FluxBeam OKCV2_MeteoraDlmm OKCV2_RaydiumCpmmSwap OKCV2_OpenBookV2 OKCV2_WhirlpoolV2 OKCV2_Phoenix OKCV2_ObricV2 OKCV2_SanctumAddLiq OKCV2_SanctumRemoveLiq OKCV2_SanctumNonWsolSwap OKCV2_SanctumWsolSwap OKCV2_PumpfunBuy OKCV2_PumpfunSell OKCV2_StabbleSwap OKCV2_SanctumRouter OKCV2_MeteoraVaultDeposit OKCV2_MeteoraVaultWithdraw OKCV2_Saros OKCV2_MeteoraLst OKCV2_Solfi OKCV2_QualiaSwap OKCV2_Zerofi OKCV2_PumpfunammBuy OKCV2_PumpfunammSell OKCV2_Virtuals OKCV2_VertigoBuy OKCV2_VertigoSell OKCV2_PerpetualsAddLiq OKCV2_PerpetualsRemoveLiq OKCV2_PerpetualsSwap OKCV2_RaydiumLaunchpad OKCV2_LetsBonkFun OKCV2_Woofi OKCV2_MeteoraDbc OKCV2_MeteoraDlmmSwap2 OKCV2_MeteoraDAMMV2 OKCV2_Gavel OKCV2_BoopfunBuy OKCV2_BoopfunSell OKCV2_MeteoraDbc2 OKCV2_GooseFX OKCV2_Dooar OKCV2_Numeraire OKCV2_SaberDecimalWrapperDeposit OKCV2_SaberDecimalWrapperWithdraw OKCV2_SarosDlmm OKCV2_OneDexSwap OKCV2_Manifest OKCV2_ByrealClmm OKCV2_PancakeSwapV3Swap OKCV2_PancakeSwapV3SwapV2 OKCV2_Tessera OKCV2_SolRfq OKCV2_Humidifi OKCV2_HeavenBuy OKCV2_HeavenSell OKCV2_SolfiV2 OKCV2_Goonfi OKCV2_MoonitBuy OKCV2_MoonitSell OKCV2_RaydiumSwapV2 OKCV2_Whalestreet OKCV2_SugarMoneyBuy OKCV2_SugarMoneySell OKCV2_MeteoraDAMMV2Swap2 OKCV2_AlphaQ OKCV2_FutarchyAmm OKCV2_PumpfunBuy2 OKCV2_PumpfunSell2 OKCV2_HumidifiSwap2 OKCV2_Scorch OKCV2_JupiterLendDeposit OKCV2_JupiterLendRedeem OKCV2_TokkaAmm ) func decodeOkxSwapTobSwapArgs(data []byte) (*OkxV2SwapArgs, error) { dec := bin.NewBorshDecoder(data) return decodeOkxV2SwapArgs(dec) } func decodeOkxSwapTobWithReceiverSwapArgs(data []byte) (*OkxV2SwapArgs, error) { dec := bin.NewBorshDecoder(data) return decodeOkxV2SwapArgs(dec) } func decodeOkxSwapTocSwapArgs(data []byte) (*OkxV2SwapArgs, error) { dec := bin.NewBorshDecoder(data) return decodeOkxV2SwapArgs(dec) } func decodeOkxSwapTocV2SwapArgs(data []byte) (*OkxV2SwapArgs, error) { dec := bin.NewBorshDecoder(data) return decodeOkxV2SwapArgs(dec) } func skipOkxV2DexPayload(dec *bin.Decoder, dex OkxV2SwapKind) error { // IMPORTANT: In IDL, Dex is an enum. Most variants have no fields, but some carry payload. // We only need to keep decoding aligned for SwapArgs.routes. switch dex { case OKCV2_SolRfq: // fields: 6*u64 + 2*bool // rfq_id, expected_maker_amount, expected_taker_amount, maker_send_amount, // taker_send_amount, expiry, maker_use_native_sol, taker_use_native_sol if err := dec.SkipBytes(8 * 6); err != nil { return err } return dec.SkipBytes(2) case OKCV2_SugarMoneyBuy, OKCV2_SugarMoneySell: // fields: u8 + u8 return dec.SkipBytes(2) case OKCV2_HumidifiSwap2: // fields: u64 return dec.SkipBytes(8) case OKCV2_Scorch: // fields: u128 => 16 bytes return dec.SkipBytes(16) default: return nil } } func decodeOkxV2SwapArgs(dec *bin.Decoder) (*OkxV2SwapArgs, error) { out := &OkxV2SwapArgs{} var err error if out.OrderID, err = dec.ReadUint64(binary.LittleEndian); err != nil { return nil, fmt.Errorf("read order_id: %w", err) } if out.AmountIn, err = dec.ReadUint64(binary.LittleEndian); err != nil { return nil, fmt.Errorf("read amount_in: %w", err) } if out.ExpectAmountOut, err = dec.ReadUint64(binary.LittleEndian); err != nil { return nil, fmt.Errorf("read expect_amount_out: %w", err) } if out.Slippage, err = dec.ReadUint16(binary.LittleEndian); err != nil { return nil, fmt.Errorf("read slippage: %w", err) } // routes: Vec routesLen, err := dec.ReadUint32(binary.LittleEndian) if err != nil { return nil, fmt.Errorf("read routes len: %w", err) } out.Routes = make([]OkxV2Route, 0, routesLen) for i := uint32(0); i < routesLen; i++ { // Route { dex: Dex(enum tag u8 [+ payload]), weight: u16, index: u8 } tag, err := dec.ReadUint8() if err != nil { return nil, fmt.Errorf("read routes[%d].dex: %w", i, err) } dex := OkxV2SwapKind(tag) if err := skipOkxV2DexPayload(dec, dex); err != nil { return nil, fmt.Errorf("skip routes[%d].dex payload (%d): %w", i, tag, err) } weight, err := dec.ReadUint16(binary.LittleEndian) if err != nil { return nil, fmt.Errorf("read routes[%d].weight: %w", i, err) } idx, err := dec.ReadUint8() if err != nil { return nil, fmt.Errorf("read routes[%d].index: %w", i, err) } out.Routes = append(out.Routes, OkxV2Route{Dex: dex, Weight: weight, Index: idx}) } return out, nil } type OkxV2SwapSolRfq struct { RfqId uint64 expectedMakerAmount uint64 expectedTakerAmount uint64 makerSendAmount uint64 takerSendAmount uint64 expiry uint64 makerUseNativeSol bool takerUseNativeSol bool } type OkxV2SwapSugarMoney struct { BondingCurveBump uint8 BondingCurveSolAssociatedAccountBump uint8 } type OkxV2SwapHumidifiSwap2 struct { SwapId uint64 } type OkxV2SwapScorch struct { Id [16]byte } func parseOkxDexRouteV2Instruction(tx TransactionGetter, accounts []uint8, data []byte) (*TxSignal, error) { if len(data) < 8 { return nil, nil } disc := data[:8] data = data[8:] var ( args *OkxV2SwapArgs err error ) switch { case bytes.Equal(disc, okxSwapTobDisc): args, err = decodeOkxSwapTobSwapArgs(data) if err != nil { return nil, fmt.Errorf("decode swap_tob args: %w", err) } case bytes.Equal(disc, okxSwapTobWithReceiverDisc): args, err = decodeOkxSwapTobWithReceiverSwapArgs(data) if err != nil { return nil, fmt.Errorf("decode swap_tob_with_receiver args: %w", err) } case bytes.Equal(disc, okxSwapTocDisc): args, err = decodeOkxSwapTocSwapArgs(data) if err != nil { return nil, fmt.Errorf("decode swap_toc args: %w", err) } case bytes.Equal(disc, okxSwapTocV2Disc): args, err = decodeOkxSwapTocV2SwapArgs(data) if err != nil { return nil, fmt.Errorf("decode swap_toc_v2 args: %w", err) } default: return nil, nil } if len(accounts) < 15 { return nil, fmt.Errorf("invalid account count: %d", len(accounts)) } var ( inputAmount uint64 routeCount int ) for _, route := range args.Routes { if route.Index == 1 && (route.Dex == OKCV2_PumpfunammSell || route.Dex == OKCV2_PumpfunSell2) { routeCount++ inputAmount = args.AmountIn * uint64(route.Weight) / 10000 } } if routeCount > 1 { logger.Warn("pumpSwapSell at inputIdx=0: multiple instances found", "tx", tx.Signatures(), "routeCount", routeCount) return nil, nil } if inputAmount == 0 { return nil, nil } srcMint, err := tx.GetAccount(accounts[3]) //getStaticKey(tx.Message.StaticAccountKeys, int(ix.Accounts[3])) var ( srcIdx uint8 ) if len(accounts) <= 15 { return nil, nil } accounts = accounts[14:] for i, acctIdx := range accounts { key, err := tx.GetAccount(acctIdx) // getStaticKey(tx.Message.StaticAccountKeys, int(acctIdx)) if err != nil { return nil, err } if key.Equals(pumpAmmProgramID) { srcIdx = uint8(i + 6) break } } if srcIdx == 0 || int(srcIdx+1) >= len(accounts) { return nil, nil } baseMint, err := tx.GetAccount(accounts[srcIdx]) // getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx])) if err != nil { return nil, err } if !baseMint.Equals(srcMint) { return nil, nil } quoteMint, err := tx.GetAccount(accounts[srcIdx+1]) // getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx+1])) if err != nil { return nil, err } if !quoteMint.Equals(solana.WrappedSol) { return nil, nil } maker, _ := tx.GetAccount(0) return &TxSignal{ TxHash: tx.Signatures(), Maker: maker.String(), Token0Address: baseMint.String(), Token1Address: wsolMint, Token0Amount: formatTokenAmount(inputAmount), Token1Amount: decimal.Zero, Event: "sell", Program: "PumpAMM", IsProcessed: false, IsToken2022: false, IsMayhemMode: false, ExactSOL: false, Token0AmountUint64: inputAmount, Block: tx.Block(), Token1AmountUint64: 0, }, nil }