[parser]: add maestro pumpamm
This commit is contained in:
257
pkg/shreder/program_maestro.go
Normal file
257
pkg/shreder/program_maestro.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -76,6 +76,7 @@ var (
|
|||||||
dlmmProgramID: {parseDlmmInstruction, "dlmm"},
|
dlmmProgramID: {parseDlmmInstruction, "dlmm"},
|
||||||
dbotProgramID: {parseDbotInstruction, "dbot"},
|
dbotProgramID: {parseDbotInstruction, "dbot"},
|
||||||
tradewizProgramID: {parseTradewizInstruction, "tradewiz"},
|
tradewizProgramID: {parseTradewizInstruction, "tradewiz"},
|
||||||
|
maestroProgramId: {parseMaestroInstruction, "maestro"},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user