diff --git a/cmd/shreder/main.go b/cmd/shreder/main.go index fbec968..4f53dcd 100644 --- a/cmd/shreder/main.go +++ b/cmd/shreder/main.go @@ -50,6 +50,11 @@ func main() { "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4", }, }, + "okxdexroutev2": { + AccountRequired: []string{ + "proVF4pMXVaYqmy4NjniPh4pqKNfMmsihgd4wdkCX3u", + }, + }, // TODO: axiom, gmgn, etc. }) if err != nil { @@ -84,12 +89,11 @@ func main() { case txBatch := <-txCh: //jsonData, _ := json.MarshalIndent(txBatch, "", " ") for _, tx := range txBatch { - if tx.Label == "flas" { + if tx.Label == "okxdexroutev2" { if tx.Event == "buy" { - fmt.Println("===============", tx.TxHash, tx.Program, tx.Event, tx.Token0Address, "token:", tx.Token0Amount, "sol:", tx.Token1Amount) + fmt.Println("===============", tx.TxHash, tx.Event, tx.Token0Address, "token:", tx.Token0Amount, "sol:", tx.Token1Amount) } else if tx.Event == "sell" { - fmt.Println("===============", tx.TxHash, tx.Program, tx.Event, tx.Token0Address, "token:", tx.Token0Amount) - + fmt.Println("===============", tx.TxHash, tx.Event, tx.Token0Address, "token:", tx.Token0Amount) } } } diff --git a/pkg/shreder/juptierv6.go b/pkg/shreder/juptierv6.go index 8ca669a..2ae3af1 100644 --- a/pkg/shreder/juptierv6.go +++ b/pkg/shreder/juptierv6.go @@ -948,15 +948,17 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) ( break } } - if srcIdx == 0 { + if srcIdx == 0 || srcIdx+1 >= uint8(len(instruction.Accounts)) { return nil, nil } - sourceMint, err = getStaticKey(tx.Message.StaticAccountKeys, int(instruction.Accounts[srcIdx])) + baseMint, err := getStaticKey(tx.Message.StaticAccountKeys, int(instruction.Accounts[srcIdx])) if err != nil { return nil, err } - + if !sourceMint.Equals(baseMint) { + return nil, nil + } quoteMint, err := getStaticKey(tx.Message.StaticAccountKeys, int(instruction.Accounts[srcIdx+1])) if err != nil { return nil, err @@ -989,14 +991,17 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) ( break } } - if srcIdx == 0 { + if srcIdx == 0 || srcIdx+1 >= uint8(len(instruction.Accounts)) { return nil, nil } - sourceMint, err = getStaticKey(tx.Message.StaticAccountKeys, int(instruction.Accounts[srcIdx])) + baseMint, err := getStaticKey(tx.Message.StaticAccountKeys, int(instruction.Accounts[srcIdx])) if err != nil { return nil, err } + if !sourceMint.Equals(baseMint) { + return nil, nil + } quoteMint, err := getStaticKey(tx.Message.StaticAccountKeys, int(instruction.Accounts[srcIdx+1])) if err != nil { @@ -1026,7 +1031,7 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) ( break } } - if srcIdx == 0 { + if srcIdx == 0 || srcIdx+1 >= uint8(len(instruction.Accounts)) { return nil, nil } sourceMint, err = getStaticKey(tx.Message.StaticAccountKeys, int(instruction.Accounts[srcIdx])) diff --git a/pkg/shreder/okxonchainlab.go b/pkg/shreder/okxonchainlab.go index 546de75..6fcb961 100644 --- a/pkg/shreder/okxonchainlab.go +++ b/pkg/shreder/okxonchainlab.go @@ -1,5 +1,367 @@ package shreder -//func parseOkxDexRouteV2Instruction(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) { -// -//} +import ( + "bytes" + "encoding/binary" + "fmt" + + bin "github.com/gagliardetto/binary" + "github.com/gagliardetto/solana-go" + "github.com/shopspring/decimal" +) + +var ( + okxDexRouteV2ProgramID = solana.MustPublicKeyFromBase58("proVF4pMXVaYqmy4NjniPh4pqKNfMmsihgd4wdkCX3u") + + 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) (*TxSignal, error) { + msg := tx.Message + if instructionIndex >= len(msg.Instructions) { + return nil, fmt.Errorf("instruction index out of bounds") + } + ix := msg.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 ( + 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[0].String(), "routeCount", routeCount) + return nil, nil + } + if inputAmount == 0 { + return nil, nil + } + + srcMint, err := getStaticKey(tx.Message.StaticAccountKeys, int(ix.Accounts[3])) + + var ( + srcIdx uint8 + ) + for i, acctIdx := range ix.Accounts { + if i < 15 { + continue + } + key, err := 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(ix.Accounts) { + return nil, nil + } + + baseMint, err := getStaticKey(tx.Message.StaticAccountKeys, int(ix.Accounts[srcIdx])) + if err != nil { + return nil, err + } + if !baseMint.Equals(srcMint) { + return nil, nil + } + + quoteMint, err := getStaticKey(tx.Message.StaticAccountKeys, int(ix.Accounts[srcIdx+1])) + if err != nil { + return nil, err + } + if !quoteMint.Equals(solana.WrappedSol) { + return nil, nil + } + + return &TxSignal{ + Label: "okxdexroutev2", + TxHash: tx.Signatures[0].String(), + Maker: tx.Message.StaticAccountKeys[0].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, + Token1AmountUint64: 0, + }, nil +} diff --git a/pkg/shreder/txparser.go b/pkg/shreder/txparser.go index fcc7082..d0931d0 100644 --- a/pkg/shreder/txparser.go +++ b/pkg/shreder/txparser.go @@ -275,6 +275,9 @@ func ParseTransaction(update *SubscribeUpdateTransaction, loader *AddressTables) case jupiterV6ProgramID: txRes, err := parseJupiterV6Instruction(versioned, i) parsed = appendParsed(parsed, txRes, err, txHash, "jupiterv6", jupiterV6ProgramID.String()) + case okxDexRouteV2ProgramID: + txRes, err := parseOkxDexRouteV2Instruction(versioned, i) + parsed = appendParsed(parsed, txRes, err, txHash, "okxdexroutev2", okxDexRouteV2ProgramID.String()) } }