diff --git a/pkg/shreder/program_maestro.go b/pkg/shreder/program_maestro.go new file mode 100644 index 0000000..acfea1c --- /dev/null +++ b/pkg/shreder/program_maestro.go @@ -0,0 +1,257 @@ +package shreder + +import ( + "bytes" + "encoding/binary" + "fmt" + + "github.com/gagliardetto/solana-go" +) + +var ( + maestroProgramId = solana.MustPublicKeyFromBase58("MaestroAAe9ge5HTc64VbBQZ6fP77pwvrhM8i1XWSAx") + maestroMultiSwap2Discriminator = [8]byte{132, 9, 212, 45, 39, 113, 215, 54} +) + +type MaestroMultiSwap2Route struct { + DexID uint8 + RouteKeyIdx uint8 +} + +// MaestroMultiSwap2Args is the decoded payload of multi_swap2 instruction. +// Payload layout (without 8-byte discriminator): +// amount_in(u64), min_amount_out(u64), route0_weight(u16), route_len(u32), +// routes(route_len * {dex_id(u8), reserved(u8), route_key_idx(u8)}), route_family(u8), route_flags(u8) +type MaestroMultiSwap2Args struct { + HasDiscriminator bool + AmountIn uint64 + MinAmountOut uint64 + SlippageBps uint16 + RouteLen uint32 + Routes []MaestroMultiSwap2Route + RouteFamily uint8 + RouteFlags uint8 + Extra []byte +} + +// decodeMaestroMultiSwap2Args decodes instruction bytes with or without the 8-byte multi_swap2 discriminator. +func decodeMaestroMultiSwap2Args(data []byte) (*MaestroMultiSwap2Args, error) { + if len(data) == 0 { + return nil, fmt.Errorf("empty data") + } + + payload := data + out := &MaestroMultiSwap2Args{} + + if len(data) >= len(maestroMultiSwap2Discriminator) && bytes.Equal(data[:8], maestroMultiSwap2Discriminator[:]) { + out.HasDiscriminator = true + payload = data[8:] + } + + const minPayloadLen = 24 // fixed fields + route_family + route_flags when route_len==0 + if len(payload) < minPayloadLen { + return nil, fmt.Errorf("payload too short: got %d, need at least %d", len(payload), minPayloadLen) + } + + out.AmountIn = binary.LittleEndian.Uint64(payload[0:8]) + out.MinAmountOut = binary.LittleEndian.Uint64(payload[8:16]) + out.SlippageBps = binary.LittleEndian.Uint16(payload[16:18]) + out.RouteLen = binary.LittleEndian.Uint32(payload[18:22]) + + needed := uint64(minPayloadLen) + uint64(out.RouteLen)*3 + if needed > uint64(len(payload)) { + return nil, fmt.Errorf("payload too short for routes: got %d, need %d (route_len=%d)", len(payload), needed, out.RouteLen) + } + + offset := 22 + out.Routes = make([]MaestroMultiSwap2Route, 0, out.RouteLen) + for i := uint32(0); i < out.RouteLen; i++ { + route := MaestroMultiSwap2Route{ + DexID: payload[offset], + RouteKeyIdx: payload[offset+2], + } + out.Routes = append(out.Routes, route) + offset += 3 + } + + out.RouteFamily = payload[offset] + out.RouteFlags = payload[offset+1] + offset += 2 + + if len(payload) > offset { + out.Extra = append([]byte(nil), payload[offset:]...) + } + + return out, nil +} + +// maestroMultiSwap2DexName maps observed dex ids from MultiSwap2 routes. +func maestroMultiSwap2DexName(dexID uint8) string { + switch dexID { + case 0: + return "RaydiumV4" + case 1: + return "MeteoraDLMM" + case 3: + return "RaydiumLaunchLab" + case 4: + return "PumpAMM" + case 5: + return "RaydiumCPMM" + case 6: + return "MeteoraAmmV2" + case 7: + return "RaydiumCLMM" + case 8: + return "OrcaWhirPool" + case 9: + return "MeteoraPools" + + case 10: + return "MeteoraDynamicBondingCurve" + default: + return fmt.Sprintf("Unknown(%d)", dexID) + } +} + +func parseMaestroInstruction(tx VersionedTransaction, idx int) (TxSignalBatch, error) { + if idx >= len(tx.Instructions) { + return nil, fmt.Errorf("instruction index out of bounds") + } + ix := tx.Instructions[idx] + if len(ix.Data) < 8 { + return nil, nil + } + return parseMaestroInstructionDataAndAccounts(tx, ix.Data, ix.Accounts) +} + +func parseMaestroInstructionDataAndAccounts(tx VersionedTransaction, ixData []byte, ixAccounts []uint8) (TxSignalBatch, error) { + args, err := decodeMaestroMultiSwap2Args(ixData) + if err != nil { + return nil, nil + } + // only decode pump amm + if len(args.Routes) != 1 { + return nil, nil + } + + if args.Routes[0].DexID != 4 { + return nil, nil + } + + var ( + event string + token0Amount uint64 + token1Amount uint64 + + // pool solana.PublicKey + baseMint solana.PublicKey + quoteMint solana.PublicKey + ) + + routeFlag := args.Routes[0].RouteKeyIdx + if routeFlag == 101 || routeFlag == 97 { + event = "buy" + token0Amount = args.MinAmountOut + token1Amount = args.AmountIn + } else if routeFlag == 65 || routeFlag == 71 { + event = "sell" + token0Amount = args.AmountIn + token1Amount = args.MinAmountOut + } else { + return nil, nil + } + + if routeFlag == 101 || routeFlag == 71 { + if len(ixAccounts) < 22 { + return nil, nil + } + token2022, err := tx.GetAccount(int(ixAccounts[6])) + if err != nil { + return nil, err + } + if !token2022.Equals(solana.Token2022ProgramID) { + return nil, nil + } + //pool, err = tx.GetAccount(int(ixAccounts[10])) + if event == "buy" { + quoteMint, err = tx.GetAccount(int(ixAccounts[9])) + if err != nil { + return nil, err + } + baseMint, err = tx.GetAccount(int(ixAccounts[21])) + if err != nil { + return nil, err + } + + } else { + baseMint, err = tx.GetAccount(int(ixAccounts[9])) + if err != nil { + return nil, err + } + + quoteMint, err = tx.GetAccount(int(ixAccounts[21])) + if err != nil { + return nil, err + } + + } + } else if routeFlag == 97 || routeFlag == 65 { + if len(ixAccounts) < 21 { + return nil, nil + } + tokenPro, err := tx.GetAccount(int(ixAccounts[5])) + if err != nil { + return nil, err + } + if !tokenPro.Equals(solana.TokenProgramID) { + return nil, nil + } + //pool, err = tx.GetAccount(int(ixAccounts[9])) + if event == "buy" { + quoteMint, err = tx.GetAccount(int(ixAccounts[8])) + if err != nil { + return nil, err + } + + baseMint, err = tx.GetAccount(int(ixAccounts[20])) + if err != nil { + return nil, err + } + + } else { + baseMint, err = tx.GetAccount(int(ixAccounts[8])) + if err != nil { + return nil, err + } + + quoteMint, err = tx.GetAccount(int(ixAccounts[20])) + if err != nil { + return nil, err + } + + } + } + if !quoteMint.Equals(wrappedSOL) { + return nil, nil + } + + return TxSignalBatch{ + &TxSignal{ + TxHash: tx.Signatures[0].String(), + Maker: tx.StaticAccountKeys[0].String(), + Token0Address: baseMint.String(), + Token1Address: wsolMint, + Token0Amount: formatTokenAmount(token0Amount), + Token1Amount: formatTokenAmount(token1Amount), + Event: event, + Program: "PumpAMM", + IsProcessed: false, + IsToken2022: false, + IsMayhemMode: false, + ExactSOL: event == "buy", + Token0AmountUint64: token0Amount, + Token1AmountUint64: token1Amount, + }, + }, nil +} diff --git a/pkg/shreder/txparser.go b/pkg/shreder/txparser.go index bfa392b..7bbf7dc 100644 --- a/pkg/shreder/txparser.go +++ b/pkg/shreder/txparser.go @@ -76,6 +76,7 @@ var ( dlmmProgramID: {parseDlmmInstruction, "dlmm"}, dbotProgramID: {parseDbotInstruction, "dbot"}, tradewizProgramID: {parseTradewizInstruction, "tradewiz"}, + maestroProgramId: {parseMaestroInstruction, "maestro"}, } )