diff --git a/pkg/shreder/juptierv6.go b/pkg/shreder/juptierv6.go index ec0d41d..c36e5ad 100644 --- a/pkg/shreder/juptierv6.go +++ b/pkg/shreder/juptierv6.go @@ -22,6 +22,10 @@ var ( jupiterSharedAccountsExactOutRouteV2 = []byte{53, 96, 229, 202, 216, 187, 250, 24} jupiterSharedAccountsRouteV2 = []byte{209, 152, 83, 147, 124, 254, 216, 233} + + usdcMint = solana.MustPublicKeyFromBase58("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v") + usd1Mint = solana.MustPublicKeyFromBase58("USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB") + usdtMint = solana.MustPublicKeyFromBase58("Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB") ) type Side uint8 @@ -882,6 +886,32 @@ func isPumpWrappedSell(kind SwapKind) bool { } } +func isStableMint(mint solana.PublicKey) bool { + if mint.Equals(usdcMint) { + return true + } + if mint.Equals(usd1Mint) { + return true + } + if mint.Equals(usdtMint) { + return true + } + return false +} + +func isToken1Mint(mint solana.PublicKey) bool { + return mint.Equals(solana.WrappedSol) || mint.Equals(solana.SystemProgramID) || isStableMint(mint) +} + +func isJupiterV6Token1RequiredDisc(disc []byte) bool { + return bytes.Equal(disc, jupiterRouteV2) || + bytes.Equal(disc, jupiterSharedAccountsRouteV2) || + bytes.Equal(disc, jupiterExactOutRouteV2) || + bytes.Equal(disc, jupiterSharedAccountsExactOutRouteV2) || + bytes.Equal(disc, jupiterSharedAccountsRoute) || + bytes.Equal(disc, jupiterSharedAccountsExactOutRoute) +} + func pumpWrappedAtIdx0(in uint64, out uint64, plan []RoutePlanStep) (pumpWrappedMatch, int) { var ( ret pumpWrappedMatch @@ -932,6 +962,42 @@ func pumpWrappedAtIdx0V2(in uint64, out uint64, plan []RoutePlanStepV2) (pumpWra return ret, count } +func pumpWrappedAny(plan []RoutePlanStep) (pumpWrappedMatch, int) { + var ( + ret pumpWrappedMatch + count int + ) + for _, step := range plan { + if !isPumpWrappedBuy(step.Swap.Kind) && !isPumpWrappedSell(step.Swap.Kind) { + continue + } + count++ + if count > 1 { + return pumpWrappedMatch{}, count + } + ret.IsBuy = isPumpWrappedBuy(step.Swap.Kind) + } + return ret, count +} + +func pumpWrappedAnyV2(plan []RoutePlanStepV2) (pumpWrappedMatch, int) { + var ( + ret pumpWrappedMatch + count int + ) + for _, step := range plan { + if !isPumpWrappedBuy(step.Swap.Kind) && !isPumpWrappedSell(step.Swap.Kind) { + continue + } + count++ + if count > 1 { + return pumpWrappedMatch{}, count + } + ret.IsBuy = isPumpWrappedBuy(step.Swap.Kind) + } + return ret, count +} + func findPumpFunMint(staticKeys []solana.PublicKey, accounts []uint8) (solana.PublicKey, bool, error) { for i, acctIdx := range accounts { key, err := getStaticKey(staticKeys, int(acctIdx)) @@ -953,6 +1019,43 @@ func findPumpFunMint(staticKeys []solana.PublicKey, accounts []uint8) (solana.Pu return solana.PublicKey{}, false, nil } +func jupiterV6SourceDestMints(msg versionedMessage, instruction compiledInstruction, disc []byte) (solana.PublicKey, solana.PublicKey, bool, error) { + switch { + case bytes.Equal(disc, jupiterRouteV2), + bytes.Equal(disc, jupiterSharedAccountsRouteV2), + bytes.Equal(disc, jupiterExactOutRouteV2), + bytes.Equal(disc, jupiterSharedAccountsExactOutRouteV2): + if len(instruction.Accounts) < 5 { + return solana.PublicKey{}, solana.PublicKey{}, false, fmt.Errorf("not enough accounts for jupiter v6 v2 instruction") + } + src, err := getStaticKey(msg.StaticAccountKeys, int(instruction.Accounts[3])) + if err != nil { + return solana.PublicKey{}, solana.PublicKey{}, false, err + } + dst, err := getStaticKey(msg.StaticAccountKeys, int(instruction.Accounts[4])) + if err != nil { + return solana.PublicKey{}, solana.PublicKey{}, false, err + } + return src, dst, true, nil + case bytes.Equal(disc, jupiterSharedAccountsRoute), + bytes.Equal(disc, jupiterSharedAccountsExactOutRoute): + if len(instruction.Accounts) < 9 { + return solana.PublicKey{}, solana.PublicKey{}, false, fmt.Errorf("not enough accounts for jupiter v6 shared accounts instruction") + } + src, err := getStaticKey(msg.StaticAccountKeys, int(instruction.Accounts[7])) + if err != nil { + return solana.PublicKey{}, solana.PublicKey{}, false, err + } + dst, err := getStaticKey(msg.StaticAccountKeys, int(instruction.Accounts[8])) + if err != nil { + return solana.PublicKey{}, solana.PublicKey{}, false, err + } + return src, dst, true, nil + default: + return solana.PublicKey{}, solana.PublicKey{}, false, nil + } +} + // only decodes inputIdx = 0 container pumpSwap instructions for now func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) { msg := tx.Message @@ -973,9 +1076,13 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) ( var ( sourceMint solana.PublicKey inputAmount uint64 + routeIn uint64 + routeOut uint64 planCount int wrapped pumpWrappedMatch wrappedCnt int + wrappedAny pumpWrappedMatch + wrappedAnyC int exactOut bool err error ) @@ -990,6 +1097,9 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) ( } inputAmount, planCount = pumpSwapSellAtIdx0V2(args.In, args.Plan) wrapped, wrappedCnt = pumpWrappedAtIdx0V2(args.In, args.Out, args.Plan) + wrappedAny, wrappedAnyC = pumpWrappedAnyV2(args.Plan) + routeIn = args.In + routeOut = args.Out case bytes.Equal(disc, jupiterSharedAccountsRouteV2): args, err := decodeJupiterV6SharedAccountsRouteV2Arg(instruction.Data[8:]) if err != nil { @@ -997,6 +1107,9 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) ( } inputAmount, planCount = pumpSwapSellAtIdx0V2(args.In, args.RoutePlan) wrapped, wrappedCnt = pumpWrappedAtIdx0V2(args.In, args.QuotedOut, args.RoutePlan) + wrappedAny, wrappedAnyC = pumpWrappedAnyV2(args.RoutePlan) + routeIn = args.In + routeOut = args.QuotedOut case bytes.Equal(disc, jupiterExactOutRouteV2): args, err := decodeJupiterV6ExactOutRouteV2Arg(instruction.Data[8:]) if err != nil { @@ -1004,6 +1117,9 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) ( } exactOut = true wrapped, wrappedCnt = pumpWrappedAtIdx0V2(args.QuotedIn, args.Out, args.RoutePlan) + wrappedAny, wrappedAnyC = pumpWrappedAnyV2(args.RoutePlan) + routeIn = args.QuotedIn + routeOut = args.Out case bytes.Equal(disc, jupiterSharedAccountsExactOutRouteV2): args, err := decodeJupiterV6SharedAccountsExactOutRouteV2Arg(instruction.Data[8:]) if err != nil { @@ -1011,6 +1127,9 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) ( } exactOut = true wrapped, wrappedCnt = pumpWrappedAtIdx0V2(args.QuotedIn, args.Out, args.RoutePlan) + wrappedAny, wrappedAnyC = pumpWrappedAnyV2(args.RoutePlan) + routeIn = args.QuotedIn + routeOut = args.Out case bytes.Equal(disc, jupiterRoute): args, err := decodeJupiterV6RouteArg(instruction.Data[8:]) if err != nil { @@ -1019,6 +1138,9 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) ( _ = args inputAmount, planCount = pumpSwapSellAtIdx0(args.In, args.Plan) wrapped, wrappedCnt = pumpWrappedAtIdx0(args.In, args.QuotedOut, args.Plan) + wrappedAny, wrappedAnyC = pumpWrappedAny(args.Plan) + routeIn = args.In + routeOut = args.QuotedOut case bytes.Equal(disc, jupiterSharedAccountsExactOutRoute): args, err := decodeJupiterV6SharedAccountsExactOutRouteArg(instruction.Data[8:]) if err != nil { @@ -1026,6 +1148,9 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) ( } exactOut = true wrapped, wrappedCnt = pumpWrappedAtIdx0(args.QuotedIn, args.Out, args.Plan) + wrappedAny, wrappedAnyC = pumpWrappedAny(args.Plan) + routeIn = args.QuotedIn + routeOut = args.Out case bytes.Equal(disc, jupiterSharedAccountsRoute): args, err := decodeJupiterV6SharedAccountsRouteArg(instruction.Data[8:]) if err != nil { @@ -1034,9 +1159,74 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) ( _ = args inputAmount, planCount = pumpSwapSellAtIdx0(args.In, args.Plan) wrapped, wrappedCnt = pumpWrappedAtIdx0(args.In, args.QuotedOut, args.Plan) + wrappedAny, wrappedAnyC = pumpWrappedAny(args.Plan) + routeIn = args.In + routeOut = args.QuotedOut default: return nil, nil } + if bytes.Equal(disc, jupiterRoute) { + if len(instruction.Accounts) < 13 { + return nil, nil + } + destMint, err := getStaticKey(tx.Message.StaticAccountKeys, int(instruction.Accounts[5])) + if err != nil { + return nil, err + } + if isToken1Mint(destMint) { + pumpKey, err := getStaticKey(tx.Message.StaticAccountKeys, int(instruction.Accounts[9])) + if err != nil { + return nil, err + } + if !pumpKey.Equals(pumpProgramID) { + return nil, nil + } + token0Mint, err := getStaticKey(tx.Message.StaticAccountKeys, int(instruction.Accounts[12])) + if err != nil { + return nil, err + } + token0Amount := decimal.Zero + if routeIn > 0 { + token0Amount = formatTokenAmount(routeIn) + } + return &TxSignal{ + TxHash: tx.Signatures[0].String(), + Maker: tx.Message.StaticAccountKeys[0].String(), + Token0Address: token0Mint.String(), + Token1Address: destMint.String(), + Token0Amount: token0Amount, + Token1Amount: decimal.Zero, + Program: "Pump", + Event: "sell", + IsToken2022: false, + IsMayhemMode: false, + ExactSOL: false, + Block: tx.Block, + Token0AmountUint64: routeIn, + Token1AmountUint64: 0, + }, nil + } + token0Amount := decimal.Zero + if routeOut > 0 { + token0Amount = formatTokenAmount(routeOut) + } + return &TxSignal{ + TxHash: tx.Signatures[0].String(), + Maker: tx.Message.StaticAccountKeys[0].String(), + Token0Address: destMint.String(), + Token1Address: wsolMint, + Token0Amount: token0Amount, + Token1Amount: decimal.Zero, + Program: "Pump", + Event: "buy", + IsToken2022: false, + IsMayhemMode: false, + ExactSOL: false, + Block: tx.Block, + Token0AmountUint64: routeOut, + Token1AmountUint64: 0, + }, nil + } if wrappedCnt > 1 { logger.Warn("pumpWrapped at inputIdx=0: multiple instances found", "tx", tx.Signatures[0].String(), "planCount", wrappedCnt) } @@ -1048,6 +1238,31 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) ( if !ok { return nil, nil } + token1Mint := solana.WrappedSol + token1IsStable := false + srcMint, dstMint, ok, err := jupiterV6SourceDestMints(tx.Message, instruction, disc) + if err != nil { + return nil, err + } + if isJupiterV6Token1RequiredDisc(disc) { + if !ok { + return nil, nil + } + if !isToken1Mint(srcMint) && !isToken1Mint(dstMint) { + return nil, nil + } + } + if ok { + if srcMint.Equals(solana.WrappedSol) || dstMint.Equals(solana.WrappedSol) { + token1Mint = solana.WrappedSol + } else if isStableMint(srcMint) { + token1Mint = srcMint + token1IsStable = true + } else if isStableMint(dstMint) { + token1Mint = dstMint + token1IsStable = true + } + } event := "sell" exactSol := false var ( @@ -1070,13 +1285,106 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) ( } token1Amount := decimal.Zero if token1AmountUint64 > 0 { - token1Amount = formatSolAmount(token1AmountUint64) + if token1IsStable { + token1Amount = formatTokenAmount(token1AmountUint64) + } else { + token1Amount = formatSolAmount(token1AmountUint64) + } + } + token1Address := wsolMint + if token1IsStable { + token1Address = token1Mint.String() } return &TxSignal{ TxHash: tx.Signatures[0].String(), Maker: tx.Message.StaticAccountKeys[0].String(), Token0Address: mint.String(), - Token1Address: wsolMint, + Token1Address: token1Address, + Token0Amount: token0Amount, + Token1Amount: token1Amount, + Program: "Pump", + Event: event, + IsToken2022: false, + IsMayhemMode: false, + ExactSOL: exactSol, + Block: tx.Block, + Token0AmountUint64: token0AmountUint64, + Token1AmountUint64: token1AmountUint64, + }, nil + } + if wrappedAnyC > 1 { + logger.Warn("pumpWrapped at inputIdx!=0: multiple instances found", "tx", tx.Signatures[0].String(), "planCount", wrappedAnyC) + } + if wrappedAnyC == 1 && routeIn > 0 && routeOut > 0 { + mint, ok, err := findPumpFunMint(tx.Message.StaticAccountKeys, instruction.Accounts) + if err != nil { + return nil, err + } + if !ok { + return nil, nil + } + token1Mint := solana.WrappedSol + token1IsStable := false + srcMint, dstMint, ok, err := jupiterV6SourceDestMints(tx.Message, instruction, disc) + if err != nil { + return nil, err + } + if isJupiterV6Token1RequiredDisc(disc) { + if !ok { + return nil, nil + } + if !isToken1Mint(srcMint) && !isToken1Mint(dstMint) { + return nil, nil + } + } + if ok { + if srcMint.Equals(solana.WrappedSol) || dstMint.Equals(solana.WrappedSol) { + token1Mint = solana.WrappedSol + } else if isStableMint(srcMint) { + token1Mint = srcMint + token1IsStable = true + } else if isStableMint(dstMint) { + token1Mint = dstMint + token1IsStable = true + } + } + event := "sell" + exactSol := false + var ( + token0AmountUint64 uint64 + token1AmountUint64 uint64 + ) + if wrappedAny.IsBuy { + event = "buy" + exactSol = !exactOut + token0AmountUint64 = routeOut + token1AmountUint64 = routeIn + } else { + exactSol = exactOut && routeOut > 0 + token0AmountUint64 = routeIn + token1AmountUint64 = routeOut + } + token0Amount := decimal.Zero + if token0AmountUint64 > 0 { + token0Amount = formatTokenAmount(token0AmountUint64) + } + token1Amount := decimal.Zero + if token1AmountUint64 > 0 { + if token1IsStable { + token1Amount = formatTokenAmount(token1AmountUint64) + } else { + token1Amount = formatSolAmount(token1AmountUint64) + } + } + token1Address := wsolMint + if token1IsStable { + token1Address = token1Mint.String() + } + return &TxSignal{ + TxHash: tx.Signatures[0].String(), + Maker: tx.Message.StaticAccountKeys[0].String(), + Token0Address: mint.String(), + Token1Address: token1Address, Token0Amount: token0Amount, Token1Amount: token1Amount, Program: "Pump",