diff --git a/pkg/shreder/juptierv6.go b/pkg/shreder/juptierv6.go index c36e5ad..72b283d 100644 --- a/pkg/shreder/juptierv6.go +++ b/pkg/shreder/juptierv6.go @@ -823,21 +823,43 @@ func decodeJupiterV6SharedAccountsRouteV2Arg(data []byte) (*JupiterV6SharedAccou return &JupiterV6SharedAccountsRouteV2Arg{ID: id, In: inAmt, QuotedOut: quotedOut, Slippage: slippage, PlatFee: pf, PosSlip: pos, RoutePlan: plan}, nil } +func isInputIdx0(idx uint8) bool { + return idx == 0 +} + +func isPumpSwapSellKind(kind SwapKind) bool { + switch kind { + case PumpSwapSell, PumpSwapSellV2, PumpSwapSellV3: + return true + default: + return false + } +} + +func isPumpSwapBuyKind(kind SwapKind) bool { + switch kind { + case PumpSwapBuy, PumpSwapBuyV2, PumpSwapBuyV3: + return true + default: + return false + } +} + func pumpSwapSellAtIdx0(amount uint64, plan []RoutePlanStep) (uint64, int) { var ( ret uint64 i int ) for _, step := range plan { - if step.InputIdx == 0 && - (step.Swap.Kind == PumpSwapSell || step.Swap.Kind == PumpSwapSellV2 || step.Swap.Kind == PumpSwapSellV3) { - i++ - if ret > 0 { - // multiple pumpSwapSell at inputIdx=0? should not happen - return 0, i - } - ret += amount * uint64(step.Percent) / 100 + if !isInputIdx0(step.InputIdx) || !isPumpSwapSellKind(step.Swap.Kind) { + continue } + i++ + if ret > 0 { + // multiple pumpSwapSell at inputIdx=0? should not happen + return 0, i + } + ret += amount * uint64(step.Percent) / 100 } return ret, i } @@ -848,20 +870,66 @@ func pumpSwapSellAtIdx0V2(amount uint64, plan []RoutePlanStepV2) (uint64, int) { i int ) for _, step := range plan { - if step.InputIdx == 0 && - (step.Swap.Kind == PumpSwapSell || step.Swap.Kind == PumpSwapSellV2 || step.Swap.Kind == PumpSwapSellV3) { - i++ - if ret > 0 { - // multiple pumpSwapSell at inputIdx=0? should not happen - - return 0, i - } - ret += amount * uint64(step.Bps) / 10000 + if !isInputIdx0(step.InputIdx) || !isPumpSwapSellKind(step.Swap.Kind) { + continue } + i++ + if ret > 0 { + // multiple pumpSwapSell at inputIdx=0? should not happen + return 0, i + } + ret += amount * uint64(step.Bps) / 10000 } return ret, i } +type pumpSwapBuyMatch struct { + InAmount uint64 + OutAmount uint64 +} + +func pumpSwapBuyAtIdx0(in uint64, out uint64, plan []RoutePlanStep) (pumpSwapBuyMatch, int) { + var ( + ret pumpSwapBuyMatch + count int + ) + for _, step := range plan { + if !isInputIdx0(step.InputIdx) || !isPumpSwapBuyKind(step.Swap.Kind) { + continue + } + count++ + if count > 1 { + return pumpSwapBuyMatch{}, count + } + ret.InAmount = in * uint64(step.Percent) / 100 + if step.Percent == 100 { + ret.OutAmount = out + } + } + return ret, count +} + +func pumpSwapBuyAtIdx0V2(in uint64, out uint64, plan []RoutePlanStepV2) (pumpSwapBuyMatch, int) { + var ( + ret pumpSwapBuyMatch + count int + ) + for _, step := range plan { + if !isInputIdx0(step.InputIdx) || !isPumpSwapBuyKind(step.Swap.Kind) { + continue + } + count++ + if count > 1 { + return pumpSwapBuyMatch{}, count + } + ret.InAmount = in * uint64(step.Bps) / 10000 + if step.Bps == 10000 { + ret.OutAmount = out + } + } + return ret, count +} + type pumpWrappedMatch struct { IsBuy bool InAmount uint64 @@ -886,6 +954,10 @@ func isPumpWrappedSell(kind SwapKind) bool { } } +func isPumpWrappedKind(kind SwapKind) bool { + return isPumpWrappedBuy(kind) || isPumpWrappedSell(kind) +} + func isStableMint(mint solana.PublicKey) bool { if mint.Equals(usdcMint) { return true @@ -918,10 +990,10 @@ func pumpWrappedAtIdx0(in uint64, out uint64, plan []RoutePlanStep) (pumpWrapped count int ) for _, step := range plan { - if step.InputIdx != 0 { + if !isInputIdx0(step.InputIdx) { continue } - if !isPumpWrappedBuy(step.Swap.Kind) && !isPumpWrappedSell(step.Swap.Kind) { + if !isPumpWrappedKind(step.Swap.Kind) { continue } count++ @@ -943,10 +1015,10 @@ func pumpWrappedAtIdx0V2(in uint64, out uint64, plan []RoutePlanStepV2) (pumpWra count int ) for _, step := range plan { - if step.InputIdx != 0 { + if !isInputIdx0(step.InputIdx) { continue } - if !isPumpWrappedBuy(step.Swap.Kind) && !isPumpWrappedSell(step.Swap.Kind) { + if !isPumpWrappedKind(step.Swap.Kind) { continue } count++ @@ -968,7 +1040,7 @@ func pumpWrappedAny(plan []RoutePlanStep) (pumpWrappedMatch, int) { count int ) for _, step := range plan { - if !isPumpWrappedBuy(step.Swap.Kind) && !isPumpWrappedSell(step.Swap.Kind) { + if !isPumpWrappedKind(step.Swap.Kind) { continue } count++ @@ -986,7 +1058,7 @@ func pumpWrappedAnyV2(plan []RoutePlanStepV2) (pumpWrappedMatch, int) { count int ) for _, step := range plan { - if !isPumpWrappedBuy(step.Swap.Kind) && !isPumpWrappedSell(step.Swap.Kind) { + if !isPumpWrappedKind(step.Swap.Kind) { continue } count++ @@ -998,6 +1070,124 @@ func pumpWrappedAnyV2(plan []RoutePlanStepV2) (pumpWrappedMatch, int) { return ret, count } +func pumpRoutePlanStats(in uint64, out uint64, plan []RoutePlanStep, includeInput bool) (uint64, int, pumpSwapBuyMatch, int, pumpWrappedMatch, int, pumpWrappedMatch, int) { + var ( + inputAmount uint64 + planCount int + ) + if includeInput { + inputAmount, planCount = pumpSwapSellAtIdx0(in, plan) + } + buySwap, buySwapCnt := pumpSwapBuyAtIdx0(in, out, plan) + wrapped, wrappedCnt := pumpWrappedAtIdx0(in, out, plan) + wrappedAny, wrappedAnyC := pumpWrappedAny(plan) + return inputAmount, planCount, buySwap, buySwapCnt, wrapped, wrappedCnt, wrappedAny, wrappedAnyC +} + +func pumpRoutePlanStatsV2(in uint64, out uint64, plan []RoutePlanStepV2, includeInput bool) (uint64, int, pumpSwapBuyMatch, int, pumpWrappedMatch, int, pumpWrappedMatch, int) { + var ( + inputAmount uint64 + planCount int + ) + if includeInput { + inputAmount, planCount = pumpSwapSellAtIdx0V2(in, plan) + } + buySwap, buySwapCnt := pumpSwapBuyAtIdx0V2(in, out, plan) + wrapped, wrappedCnt := pumpWrappedAtIdx0V2(in, out, plan) + wrappedAny, wrappedAnyC := pumpWrappedAnyV2(plan) + return inputAmount, planCount, buySwap, buySwapCnt, wrapped, wrappedCnt, wrappedAny, wrappedAnyC +} + +func parseJupiterPumpAmmRoute(tx *versionedTransaction, instruction compiledInstruction, in uint64, out uint64, plan []RoutePlanStep) (*TxSignal, bool, error) { + var ( + isBuy bool + isSell bool + count int + ) + for _, step := range plan { + if !isInputIdx0(step.InputIdx) { + continue + } + if isPumpSwapSellKind(step.Swap.Kind) { + isSell = true + count++ + } else if isPumpSwapBuyKind(step.Swap.Kind) { + isBuy = true + count++ + } + } + if count == 0 { + return nil, false, nil + } + if count > 1 || (isBuy && isSell) { + logger.Warn("pumpamm route at inputIdx=0: multiple instances found", "tx", tx.Signatures[0].String(), "planCount", count) + return nil, true, nil + } + if len(instruction.Accounts) < 14 { + return nil, true, nil + } + token0Key, err := getStaticKey(tx.Message.StaticAccountKeys, int(instruction.Accounts[13])) + if err != nil { + return nil, true, err + } + if isSell { + token0Amount := decimal.Zero + if in > 0 { + token0Amount = formatTokenAmount(in) + } + return &TxSignal{ + TxHash: tx.Signatures[0].String(), + Maker: tx.Message.StaticAccountKeys[0].String(), + Token0Address: token0Key.String(), + Token1Address: wsolMint, + Token0Amount: token0Amount, + Token1Amount: decimal.Zero, + Program: "PumpAMM", + Event: "sell", + IsToken2022: false, + IsMayhemMode: false, + ExactSOL: false, + Block: tx.Block, + Token0AmountUint64: in, + Token1AmountUint64: 0, + }, true, nil + } + if len(instruction.Accounts) < 15 { + return nil, true, nil + } + wsolKey, err := getStaticKey(tx.Message.StaticAccountKeys, int(instruction.Accounts[14])) + if err != nil { + return nil, true, err + } + if !wsolKey.Equals(solana.WrappedSol) { + return nil, true, nil + } + token0Amount := decimal.Zero + if out > 0 { + token0Amount = formatTokenAmount(out) + } + token1Amount := decimal.Zero + if in > 0 { + token1Amount = formatSolAmount(in) + } + return &TxSignal{ + TxHash: tx.Signatures[0].String(), + Maker: tx.Message.StaticAccountKeys[0].String(), + Token0Address: token0Key.String(), + Token1Address: wsolMint, + Token0Amount: token0Amount, + Token1Amount: token1Amount, + Program: "PumpAMM", + Event: "buy", + IsToken2022: false, + IsMayhemMode: false, + ExactSOL: true, + Block: tx.Block, + Token0AmountUint64: out, + Token1AmountUint64: in, + }, true, nil +} + func findPumpFunMint(staticKeys []solana.PublicKey, accounts []uint8) (solana.PublicKey, bool, error) { for i, acctIdx := range accounts { key, err := getStaticKey(staticKeys, int(acctIdx)) @@ -1079,6 +1269,8 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) ( routeIn uint64 routeOut uint64 planCount int + buySwap pumpSwapBuyMatch + buySwapCnt int wrapped pumpWrappedMatch wrappedCnt int wrappedAny pumpWrappedMatch @@ -1095,9 +1287,7 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) ( if err != nil { return nil, err } - inputAmount, planCount = pumpSwapSellAtIdx0V2(args.In, args.Plan) - wrapped, wrappedCnt = pumpWrappedAtIdx0V2(args.In, args.Out, args.Plan) - wrappedAny, wrappedAnyC = pumpWrappedAnyV2(args.Plan) + inputAmount, planCount, buySwap, buySwapCnt, wrapped, wrappedCnt, wrappedAny, wrappedAnyC = pumpRoutePlanStatsV2(args.In, args.Out, args.Plan, true) routeIn = args.In routeOut = args.Out case bytes.Equal(disc, jupiterSharedAccountsRouteV2): @@ -1105,9 +1295,7 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) ( if err != nil { return nil, err } - inputAmount, planCount = pumpSwapSellAtIdx0V2(args.In, args.RoutePlan) - wrapped, wrappedCnt = pumpWrappedAtIdx0V2(args.In, args.QuotedOut, args.RoutePlan) - wrappedAny, wrappedAnyC = pumpWrappedAnyV2(args.RoutePlan) + inputAmount, planCount, buySwap, buySwapCnt, wrapped, wrappedCnt, wrappedAny, wrappedAnyC = pumpRoutePlanStatsV2(args.In, args.QuotedOut, args.RoutePlan, true) routeIn = args.In routeOut = args.QuotedOut case bytes.Equal(disc, jupiterExactOutRouteV2): @@ -1116,8 +1304,7 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) ( return nil, err } exactOut = true - wrapped, wrappedCnt = pumpWrappedAtIdx0V2(args.QuotedIn, args.Out, args.RoutePlan) - wrappedAny, wrappedAnyC = pumpWrappedAnyV2(args.RoutePlan) + inputAmount, planCount, buySwap, buySwapCnt, wrapped, wrappedCnt, wrappedAny, wrappedAnyC = pumpRoutePlanStatsV2(args.QuotedIn, args.Out, args.RoutePlan, false) routeIn = args.QuotedIn routeOut = args.Out case bytes.Equal(disc, jupiterSharedAccountsExactOutRouteV2): @@ -1126,8 +1313,7 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) ( return nil, err } exactOut = true - wrapped, wrappedCnt = pumpWrappedAtIdx0V2(args.QuotedIn, args.Out, args.RoutePlan) - wrappedAny, wrappedAnyC = pumpWrappedAnyV2(args.RoutePlan) + inputAmount, planCount, buySwap, buySwapCnt, wrapped, wrappedCnt, wrappedAny, wrappedAnyC = pumpRoutePlanStatsV2(args.QuotedIn, args.Out, args.RoutePlan, false) routeIn = args.QuotedIn routeOut = args.Out case bytes.Equal(disc, jupiterRoute): @@ -1135,10 +1321,14 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) ( if err != nil { return nil, err } - _ = args - inputAmount, planCount = pumpSwapSellAtIdx0(args.In, args.Plan) - wrapped, wrappedCnt = pumpWrappedAtIdx0(args.In, args.QuotedOut, args.Plan) - wrappedAny, wrappedAnyC = pumpWrappedAny(args.Plan) + sig, handled, err := parseJupiterPumpAmmRoute(tx, instruction, args.In, args.QuotedOut, args.Plan) + if err != nil { + return nil, err + } + if handled { + return sig, nil + } + inputAmount, planCount, buySwap, buySwapCnt, wrapped, wrappedCnt, wrappedAny, wrappedAnyC = pumpRoutePlanStats(args.In, args.QuotedOut, args.Plan, true) routeIn = args.In routeOut = args.QuotedOut case bytes.Equal(disc, jupiterSharedAccountsExactOutRoute): @@ -1147,8 +1337,7 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) ( return nil, err } exactOut = true - wrapped, wrappedCnt = pumpWrappedAtIdx0(args.QuotedIn, args.Out, args.Plan) - wrappedAny, wrappedAnyC = pumpWrappedAny(args.Plan) + inputAmount, planCount, buySwap, buySwapCnt, wrapped, wrappedCnt, wrappedAny, wrappedAnyC = pumpRoutePlanStats(args.QuotedIn, args.Out, args.Plan, false) routeIn = args.QuotedIn routeOut = args.Out case bytes.Equal(disc, jupiterSharedAccountsRoute): @@ -1157,9 +1346,7 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) ( return nil, err } _ = args - inputAmount, planCount = pumpSwapSellAtIdx0(args.In, args.Plan) - wrapped, wrappedCnt = pumpWrappedAtIdx0(args.In, args.QuotedOut, args.Plan) - wrappedAny, wrappedAnyC = pumpWrappedAny(args.Plan) + inputAmount, planCount, buySwap, buySwapCnt, wrapped, wrappedCnt, wrappedAny, wrappedAnyC = pumpRoutePlanStats(args.In, args.QuotedOut, args.Plan, true) routeIn = args.In routeOut = args.QuotedOut default: @@ -1401,12 +1588,33 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) ( // multiple pumpSwapSell at inputIdx=0? should not happen logger.Warn("pumpSwapSell at inputIdx=0: multiple instances found", "tx", tx.Signatures[0].String(), "planCount", planCount) } - if inputAmount == 0 { + if buySwapCnt > 1 { + // multiple pumpSwapBuy at inputIdx=0? should not happen + logger.Warn("pumpSwapBuy at inputIdx=0: multiple instances found", "tx", tx.Signatures[0].String(), "planCount", buySwapCnt) + } + hasSell := inputAmount > 0 + hasBuy := buySwap.InAmount > 0 + if hasSell && hasBuy { + logger.Warn("pumpSwap buy/sell at inputIdx=0: both found", "tx", tx.Signatures[0].String(), "sellCount", planCount, "buyCount", buySwapCnt) + return nil, nil + } + if !hasSell && !hasBuy { return nil, nil } + var ( + baseMint solana.PublicKey + quoteMint solana.PublicKey + destMint solana.PublicKey + destMintOK bool + sourceMintOK bool + ) + // existing mint extraction logic only valid for route_v2/ exact_out_route_v2. Keep it but guard. - if bytes.Equal(disc, jupiterRouteV2) || bytes.Equal(disc, jupiterSharedAccountsRouteV2) { + if bytes.Equal(disc, jupiterRouteV2) || + bytes.Equal(disc, jupiterSharedAccountsRouteV2) || + bytes.Equal(disc, jupiterExactOutRouteV2) || + bytes.Equal(disc, jupiterSharedAccountsExactOutRouteV2) { if len(instruction.Accounts) < 6 { return nil, fmt.Errorf("not enough accounts for jupiter v6 v2 instruction") } @@ -1414,6 +1622,12 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) ( if err != nil { return nil, err } + destMint, err = getStaticKey(tx.Message.StaticAccountKeys, int(instruction.Accounts[4])) + if err != nil { + return nil, err + } + destMintOK = true + sourceMintOK = true var ( srcIdx uint8 @@ -1436,14 +1650,11 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) ( return nil, nil } - baseMint, err := getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx])) + baseMint, err = getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx])) if err != nil { return nil, err } - if !sourceMint.Equals(baseMint) { - return nil, nil - } - quoteMint, err := getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx+1])) + quoteMint, err = getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx+1])) if err != nil { return nil, err } @@ -1451,7 +1662,7 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) ( return nil, nil } - } else if bytes.Equal(disc, jupiterSharedAccountsRoute) { + } else if bytes.Equal(disc, jupiterSharedAccountsRoute) || bytes.Equal(disc, jupiterSharedAccountsExactOutRoute) { if len(instruction.Accounts) < 12 { return nil, fmt.Errorf("not enough accounts for jupiter v6 jupiterSharedAccountsRoute instruction") } @@ -1459,6 +1670,12 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) ( if err != nil { return nil, err } + destMint, err = getStaticKey(tx.Message.StaticAccountKeys, int(instruction.Accounts[8])) + if err != nil { + return nil, err + } + destMintOK = true + sourceMintOK = true var ( srcIdx uint8 ) @@ -1480,15 +1697,12 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) ( return nil, nil } - baseMint, err := getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx])) + baseMint, err = getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx])) if err != nil { return nil, err } - if !sourceMint.Equals(baseMint) { - return nil, nil - } - quoteMint, err := getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx+1])) + quoteMint, err = getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx+1])) if err != nil { return nil, err } @@ -1517,35 +1731,72 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) ( if srcIdx == 0 || srcIdx+1 >= uint8(len(accounts)) { return nil, nil } - sourceMint, err = getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx])) + baseMint, err = getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx])) if err != nil { return nil, err } - quoteMint, err := getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx+1])) + quoteMint, err = getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx+1])) if err != nil { return nil, err } if !quoteMint.Equals(solana.WrappedSol) { return nil, nil } + sourceMint = baseMint + } + + if hasSell { + if sourceMintOK && !sourceMint.Equals(baseMint) { + return nil, nil + } + } else { + if !sourceMintOK { + return nil, nil + } + if !sourceMint.Equals(solana.WrappedSol) && !sourceMint.Equals(solana.SystemProgramID) { + return nil, nil + } + if destMintOK && !destMint.Equals(baseMint) { + return nil, nil + } + } + + event := "sell" + exactSol := false + token0AmountUint64 := inputAmount + token1AmountUint64 := uint64(0) + if hasBuy { + event = "buy" + exactSol = !exactOut + token0AmountUint64 = buySwap.OutAmount + token1AmountUint64 = buySwap.InAmount + } + + token0Amount := decimal.Zero + if token0AmountUint64 > 0 { + token0Amount = formatTokenAmount(token0AmountUint64) + } + token1Amount := decimal.Zero + if token1AmountUint64 > 0 { + token1Amount = formatSolAmount(token1AmountUint64) } signal := &TxSignal{ TxHash: tx.Signatures[0].String(), Maker: tx.Message.StaticAccountKeys[0].String(), - Token0Address: sourceMint.String(), + Token0Address: baseMint.String(), Token1Address: wsolMint, - Token0Amount: formatTokenAmount(inputAmount), - Token1Amount: decimal.Zero, + Token0Amount: token0Amount, + Token1Amount: token1Amount, Program: "PumpAMM", - Event: "sell", + Event: event, IsToken2022: false, IsMayhemMode: false, - ExactSOL: false, + ExactSOL: exactSol, Block: tx.Block, - Token0AmountUint64: inputAmount, - Token1AmountUint64: 0, + Token0AmountUint64: token0AmountUint64, + Token1AmountUint64: token1AmountUint64, } return signal, nil