Files
libsam/pkg/shreder/program_maestro.go

258 lines
6.3 KiB
Go
Raw Normal View History

2026-03-11 14:03:25 +08:00
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
}