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 VersionedTransaction, instructionIndex int) (TxSignalBatch, error) { if instructionIndex >= len(tx.Instructions) { return nil, fmt.Errorf("instruction index out of bounds") } ix := tx.Instructions[instructionIndex] if len(ix.Data) < 8 { return nil, nil } disc := ix.Data[:8] data := ix.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(ix.Accounts) < 15 { return nil, fmt.Errorf("invalid account count: %d", len(ix.Accounts)) } var ( pumpAmmSellAmount uint64 pumpAmmBuyAmount uint64 pumpFunSellAmount uint64 pumpFunBuyAmount uint64 pumpAmmSellCount int pumpAmmBuyCount int pumpFunSellCount int pumpFunBuyCount int ) for _, route := range args.Routes { if route.Index != 1 { continue } switch route.Dex { case OKCV2_PumpfunammSell: pumpAmmSellCount++ pumpAmmSellAmount = args.AmountIn * uint64(route.Weight) / 10000 case OKCV2_PumpfunammBuy: pumpAmmBuyCount++ pumpAmmBuyAmount = args.AmountIn * uint64(route.Weight) / 10000 case OKCV2_PumpfunSell2: pumpFunSellCount++ pumpFunSellAmount = args.AmountIn * uint64(route.Weight) / 10000 case OKCV2_PumpfunBuy2: pumpFunBuyCount++ pumpFunBuyAmount = args.AmountIn * uint64(route.Weight) / 10000 } } if pumpAmmSellCount > 1 { logger.Warn("pumpAmmSwapSell at inputIdx=0: multiple instances found", "tx", tx.Signatures[0].String(), "routeCount", pumpAmmSellCount) return nil, nil } if pumpAmmBuyCount > 1 { logger.Warn("pumpAmmSwapBuy at inputIdx=0: multiple instances found", "tx", tx.Signatures[0].String(), "routeCount", pumpAmmBuyCount) return nil, nil } if pumpFunSellCount > 1 { logger.Warn("pumpFunSwapSell at inputIdx=0: multiple instances found", "tx", tx.Signatures[0].String(), "routeCount", pumpFunSellCount) return nil, nil } if pumpFunBuyCount > 1 { logger.Warn("pumpFunSwapBuy at inputIdx=0: multiple instances found", "tx", tx.Signatures[0].String(), "routeCount", pumpFunBuyCount) return nil, nil } if pumpAmmSellAmount == 0 && pumpAmmBuyAmount == 0 && pumpFunSellAmount == 0 && pumpFunBuyAmount == 0 { return nil, nil } out := make(TxSignalBatch, 0, 2) if pumpFunBuyAmount > 0 || pumpFunSellAmount > 0 { if pumpFunBuyAmount > 0 { if len(ix.Accounts) < 5 { return nil, fmt.Errorf("invalid account count: %d", len(ix.Accounts)) } baseMint, err := tx.GetAccount(int(ix.Accounts[4])) if err != nil { return nil, err } out = append(out, &TxSignal{ TxHash: tx.Signatures[0].String(), Maker: tx.StaticAccountKeys[0].String(), Token0Address: baseMint.String(), Token1Address: wsolMint, Token0Amount: decimal.Zero, Token1Amount: formatSolAmount(pumpFunBuyAmount), Event: "buy", Program: "Pump", IsProcessed: false, IsToken2022: false, IsMayhemMode: false, ExactSOL: true, Token0AmountUint64: 0, Token1AmountUint64: pumpFunBuyAmount, }) } if pumpFunSellAmount > 0 { if len(ix.Accounts) < 4 { return nil, fmt.Errorf("invalid account count: %d", len(ix.Accounts)) } baseMint, err := tx.GetAccount(int(ix.Accounts[3])) if err != nil { return nil, err } out = append(out, &TxSignal{ TxHash: tx.Signatures[0].String(), Maker: tx.StaticAccountKeys[0].String(), Token0Address: baseMint.String(), Token1Address: wsolMint, Token0Amount: formatTokenAmount(pumpFunSellAmount), Token1Amount: decimal.Zero, Event: "sell", Program: "Pump", IsProcessed: false, IsToken2022: false, IsMayhemMode: false, ExactSOL: false, Token0AmountUint64: pumpFunSellAmount, Token1AmountUint64: 0, }) } } if pumpAmmBuyAmount > 0 || pumpAmmSellAmount > 0 { if len(ix.Accounts) <= 15 { if len(out) == 0 { return nil, nil } return out, nil } accounts := ix.Accounts[14:] var pumpAmmIdx uint8 for i, acctIdx := range accounts { key, err := tx.GetAccount(int(acctIdx)) if err != nil { return nil, err } if key.Equals(pumpAmmProgramID) { pumpAmmIdx = uint8(i + 6) break } } if pumpAmmIdx == 0 || int(pumpAmmIdx+1) >= len(accounts) { if len(out) == 0 { return nil, nil } return out, nil } baseMint, err := tx.GetAccount(int(accounts[pumpAmmIdx])) if err != nil { return nil, err } quoteMint, err := tx.GetAccount(int(accounts[pumpAmmIdx+1])) if err != nil { return nil, err } if !quoteMint.Equals(solana.WrappedSol) { if len(out) == 0 { return nil, nil } return out, nil } if pumpAmmBuyAmount > 0 { if len(ix.Accounts) < 5 { return nil, fmt.Errorf("invalid account count: %d", len(ix.Accounts)) } srcMint, err := tx.GetAccount(int(ix.Accounts[4])) if err != nil { return nil, err } if baseMint.Equals(srcMint) { out = append(out, &TxSignal{ TxHash: tx.Signatures[0].String(), Maker: tx.StaticAccountKeys[0].String(), Token0Address: baseMint.String(), Token1Address: wsolMint, Token0Amount: decimal.Zero, Token1Amount: formatSolAmount(pumpAmmBuyAmount), Event: "buy", Program: "PumpAMM", IsProcessed: false, IsToken2022: false, IsMayhemMode: false, ExactSOL: true, Token0AmountUint64: 0, Token1AmountUint64: pumpAmmBuyAmount, }) } } else if pumpAmmSellAmount > 0 { if len(ix.Accounts) < 4 { return nil, fmt.Errorf("invalid account count: %d", len(ix.Accounts)) } srcMint, err := tx.GetAccount(int(ix.Accounts[3])) if err != nil { return nil, err } if baseMint.Equals(srcMint) { out = append(out, &TxSignal{ TxHash: tx.Signatures[0].String(), Maker: tx.StaticAccountKeys[0].String(), Token0Address: baseMint.String(), Token1Address: wsolMint, Token0Amount: formatTokenAmount(pumpAmmSellAmount), Token1Amount: decimal.Zero, Event: "sell", Program: "PumpAMM", IsProcessed: false, IsToken2022: false, IsMayhemMode: false, ExactSOL: false, Token0AmountUint64: pumpAmmSellAmount, Token1AmountUint64: 0, }) } } } if len(out) == 0 { return nil, nil } return out, nil }