1556 lines
42 KiB
Go
1556 lines
42 KiB
Go
package shreder
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"fmt"
|
|
|
|
bin "github.com/gagliardetto/binary"
|
|
"github.com/gagliardetto/solana-go"
|
|
"github.com/shopspring/decimal"
|
|
)
|
|
|
|
var (
|
|
jupiterRouteV2 = []byte{187, 100, 250, 204, 49, 196, 175, 20}
|
|
jupiterExactOutRouteV2 = []byte{157, 138, 184, 82, 21, 244, 243, 36}
|
|
|
|
jupiterRoute = []byte{229, 23, 203, 151, 122, 227, 173, 42}
|
|
jupiterRouteWithTokenLedger = []byte{150, 86, 71, 116, 167, 93, 14, 104}
|
|
jupiterSharedAccountsExactOutRoute = []byte{176, 209, 105, 168, 154, 125, 69, 62}
|
|
jupiterSharedAccountsRoute = []byte{193, 32, 155, 51, 65, 214, 156, 129}
|
|
jupiterSharedAccountsRouteWithTokenLedger = []byte{230, 121, 143, 80, 119, 159, 106, 170}
|
|
|
|
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
|
|
|
|
type SwapKind uint8
|
|
|
|
const (
|
|
Saber SwapKind = iota
|
|
SaberAddDecimalsDeposit
|
|
SaberAddDecimalsWithdraw
|
|
TokenSwap
|
|
Sencha
|
|
Step
|
|
Cropper
|
|
Raydium
|
|
Crema
|
|
Lifinity
|
|
Mercurial
|
|
Cykura
|
|
Serum
|
|
MarinadeDeposit
|
|
MarinadeUnstake
|
|
Aldrin
|
|
AldrinV2
|
|
Whirlpool
|
|
Invariant
|
|
Meteora
|
|
GooseFX
|
|
DeltaFi
|
|
Balansol
|
|
MarcoPolo
|
|
Dradex
|
|
LifinityV2
|
|
RaydiumClmm
|
|
Openbook
|
|
Phoenix
|
|
Symmetry
|
|
TokenSwapV2
|
|
HeliumTreasuryManagementRedeemV0
|
|
StakeDexStakeWrappedSol
|
|
StakeDexSwapViaStake
|
|
GooseFXV2
|
|
Perps
|
|
PerpsAddLiquidity
|
|
PerpsRemoveLiquidity
|
|
MeteoraDlmm
|
|
OpenBookV2
|
|
RaydiumClmmV2
|
|
StakeDexPrefundWithdrawStakeAndDepositStake
|
|
Clone
|
|
SanctumS
|
|
SanctumSAddLiquidity
|
|
SanctumSRemoveLiquidity
|
|
RaydiumCP
|
|
WhirlpoolSwapV2
|
|
OneIntro
|
|
PumpWrappedBuy
|
|
PumpWrappedSell
|
|
PerpsV2
|
|
PerpsV2AddLiquidity
|
|
PerpsV2RemoveLiquidity
|
|
MoonshotWrappedBuy
|
|
MoonshotWrappedSell
|
|
StabbleStableSwap
|
|
StabbleWeightedSwap
|
|
Obric
|
|
FoxBuyFromEstimatedCost
|
|
FoxClaimPartial
|
|
SolFi
|
|
SolayerDelegateNoInit
|
|
SolayerUndelegateNoInit
|
|
TokenMill
|
|
DaosFunBuy
|
|
DaosFunSell
|
|
ZeroFi
|
|
StakeDexWithdrawWrappedSol
|
|
VirtualsBuy
|
|
VirtualsSell
|
|
Perena
|
|
PumpSwapBuy
|
|
PumpSwapSell
|
|
Gamma
|
|
MeteoraDlmmSwapV2
|
|
Woofi
|
|
MeteoraDammV2
|
|
MeteoraDynamicBondingCurveSwap
|
|
StabbleStableSwapV2
|
|
StabbleWeightedSwapV2
|
|
RaydiumLaunchlabBuy
|
|
RaydiumLaunchlabSell
|
|
BoopdotfunWrappedBuy
|
|
BoopdotfunWrappedSell
|
|
Plasma
|
|
GoonFi
|
|
HumidiFi
|
|
MeteoraDynamicBondingCurveSwapWithRemainingAccounts
|
|
TesseraV
|
|
PumpWrappedBuyV2
|
|
PumpWrappedSellV2
|
|
PumpSwapBuyV2
|
|
PumpSwapSellV2
|
|
Heaven
|
|
SolFiV2
|
|
Aquifer
|
|
PumpWrappedBuyV3
|
|
PumpWrappedSellV3
|
|
PumpSwapBuyV3
|
|
PumpSwapSellV3
|
|
JupiterLendDeposit
|
|
JupiterLendRedeem
|
|
DefiTuna
|
|
AlphaQ
|
|
RaydiumV2
|
|
SarosDlmm
|
|
Futarchy
|
|
MeteoraDammV2WithRemainingAccounts
|
|
Obsidian
|
|
WhaleStreet
|
|
DynamicV1
|
|
PumpWrappedBuyV4
|
|
PumpWrappedSellV4
|
|
CarrotIssue
|
|
CarrotRedeem
|
|
Manifest
|
|
BisonFi
|
|
HumidiFiV2
|
|
PerenaStar
|
|
JupiterRfqV2
|
|
GoonFiV2
|
|
)
|
|
|
|
var swapKindNames = [122]string{"Saber", "SaberAddDecimalsDeposit", "SaberAddDecimalsWithdraw", "TokenSwap", "Sencha", "Step", "Cropper",
|
|
"Raydium", "Crema", "Lifinity", "Mercurial", "Cykura", "Serum", "MarinadeDeposit", "MarinadeUnstake", "Aldrin", "AldrinV2", "Whirlpool",
|
|
"Invariant", "Meteora", "GooseFX", "DeltaFi", "Balansol", "MarcoPolo", "Dradex", "LifinityV2", "RaydiumClmm", "Openbook", "Phoenix",
|
|
"Symmetry", "TokenSwapV2", "HeliumTreasuryManagementRedeemV0", "StakeDexStakeWrappedSol", "StakeDexSwapViaStake", "GooseFXV2", "Perps",
|
|
"PerpsAddLiquidity", "PerpsRemoveLiquidity", "MeteoraDlmm", "OpenBookV2", "RaydiumClmmV2", "StakeDexPrefundWithdrawStakeAndDepositStake",
|
|
"Clone", "SanctumS", "SanctumSAddLiquidity", "SanctumSRemoveLiquidity", "RaydiumCP", "WhirlpoolSwapV2", "OneIntro", "PumpWrappedBuy",
|
|
"PumpWrappedSell", "PerpsV2", "PerpsV2AddLiquidity", "PerpsV2RemoveLiquidity", "MoonshotWrappedBuy", "MoonshotWrappedSell", "StabbleStableSwap",
|
|
"StabbleWeightedSwap", "Obric", "FoxBuyFromEstimatedCost", "FoxClaimPartial", "SolFi", "SolayerDelegateNoInit", "SolayerUndelegateNoInit", "TokenMill",
|
|
"DaosFunBuy", "DaosFunSell", "ZeroFi", "StakeDexWithdrawWrappedSol", "VirtualsBuy", "VirtualsSell", "Perena", "PumpSwapBuy", "PumpSwapSell", "Gamma",
|
|
"MeteoraDlmmSwapV2", "Woofi", "MeteoraDammV2", "MeteoraDynamicBondingCurveSwap", "StabbleStableSwapV2", "StabbleWeightedSwapV2", "RaydiumLaunchlabBuy",
|
|
"RaydiumLaunchlabSell", "BoopdotfunWrappedBuy", "BoopdotfunWrappedSell", "Plasma", "GoonFi", "HumidiFi",
|
|
"MeteoraDynamicBondingCurveSwapWithRemainingAccounts", "TesseraV", "PumpWrappedBuyV2", "PumpWrappedSellV2", "PumpSwapBuyV2", "PumpSwapSellV2",
|
|
"Heaven", "SolFiV2", "Aquifer", "PumpWrappedBuyV3", "PumpWrappedSellV3", "PumpSwapBuyV3", "PumpSwapSellV3", "JupiterLendDeposit", "JupiterLendRedeem",
|
|
"DefiTuna", "AlphaQ", "RaydiumV2", "SarosDlmm", "Futarchy", "MeteoraDammV2WithRemainingAccounts", "Obsidian", "WhaleStreet", "DynamicV1",
|
|
"PumpWrappedBuyV4", "PumpWrappedSellV4", "CarrotIssue", "CarrotRedeem", "Manifest", "BisonFi", "HumidiFiV2", "PerenaStar", "JupiterRfqV2", "GoonFiV2"}
|
|
|
|
func (s SwapKind) String() string {
|
|
idx := int(s)
|
|
if idx < 0 || idx >= len(swapKindNames) {
|
|
return fmt.Sprintf("SwapKind(%d)", uint8(s))
|
|
}
|
|
return swapKindNames[idx]
|
|
}
|
|
|
|
type Swap struct {
|
|
Kind SwapKind
|
|
}
|
|
|
|
type RoutePlanStepV2 struct {
|
|
Swap Swap
|
|
Bps uint16
|
|
|
|
InputIdx uint8
|
|
OutputIdx uint8
|
|
}
|
|
|
|
type RoutePlanStep struct {
|
|
Swap Swap
|
|
Percent uint8
|
|
|
|
InputIdx uint8
|
|
OutputIdx uint8
|
|
}
|
|
|
|
type JupiterV6RouteV2Arg struct {
|
|
In uint64
|
|
Out uint64
|
|
|
|
SlippageBps uint16
|
|
PlatformFeeBps uint16
|
|
PositiveSlippageBps uint16
|
|
|
|
Plan []RoutePlanStepV2
|
|
}
|
|
|
|
func skipRemainingAccountsInfo(dec *bin.Decoder) error {
|
|
// RemainingAccountsInfo { slices: Vec<RemainingAccountsSlice> }
|
|
// RemainingAccountsSlice { accounts_type: u8, length: u8 }
|
|
ln, err := dec.ReadUint32(binary.LittleEndian)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// each slice is 2 bytes
|
|
return dec.SkipBytes(uint(ln) * 2)
|
|
}
|
|
|
|
func skipOptionRemainingAccountsInfo(dec *bin.Decoder) error {
|
|
pos := dec.Position()
|
|
tag, err := dec.ReadUint8()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
switch tag {
|
|
case 0:
|
|
return nil
|
|
case 1:
|
|
return skipRemainingAccountsInfo(dec)
|
|
default:
|
|
// Version drift: sometimes a swap variant we think has Option<RemainingAccountsInfo> is actually no-payload.
|
|
_ = dec.SetPosition(pos)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func skipCandidateSwap(dec *bin.Decoder) error {
|
|
// CandidateSwap enum (this IDL variant):
|
|
// 0 HumidiFi { u64, bool }
|
|
// 1 TesseraV { Side(u8) }
|
|
// NOTE: other IDL versions may include more variants (e.g. HumidiFiV2).
|
|
tag, err := dec.ReadUint8()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
switch tag {
|
|
case 0:
|
|
// HumidiFi u64 + bool
|
|
if err := dec.SkipBytes(8); err != nil {
|
|
return err
|
|
}
|
|
return dec.SkipBytes(1)
|
|
case 1:
|
|
// TesseraV (Side: u8)
|
|
return dec.SkipBytes(1)
|
|
case 2:
|
|
// Seen in other IDLs: HumidiFiV2 { u64, bool }
|
|
if err := dec.SkipBytes(8); err != nil {
|
|
return err
|
|
}
|
|
return dec.SkipBytes(1)
|
|
default:
|
|
return fmt.Errorf("unknown CandidateSwap variant: %d", tag)
|
|
}
|
|
}
|
|
|
|
func skipDynamicV1(dec *bin.Decoder) error {
|
|
// DynamicV1 { candidate_swaps: Vec<CandidateSwap> }
|
|
ln, err := dec.ReadUint32(binary.LittleEndian)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for i := uint32(0); i < ln; i++ {
|
|
if err := skipCandidateSwap(dec); err != nil {
|
|
return fmt.Errorf("CandidateSwap[%d]: %w", i, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func decodeSwap(dec *bin.Decoder) (Swap, error) {
|
|
tag, err := dec.ReadUint8()
|
|
if err != nil {
|
|
return Swap{}, fmt.Errorf("read Swap variant: %w", err)
|
|
}
|
|
k := SwapKind(tag)
|
|
out := Swap{Kind: k}
|
|
|
|
skipU8 := func() error { return dec.SkipBytes(1) }
|
|
skipBool := func() error { return dec.SkipBytes(1) }
|
|
skipU32 := func() error { return dec.SkipBytes(4) }
|
|
skipU64 := func() error { return dec.SkipBytes(8) }
|
|
skipTwoU64 := func() error { return dec.SkipBytes(16) }
|
|
skipRemaining := func() error { return skipRemainingAccountsInfo(dec) }
|
|
skipOptRemaining := func() error { return skipOptionRemainingAccountsInfo(dec) }
|
|
|
|
switch k {
|
|
// -------- payload-less variants --------
|
|
case Saber, SaberAddDecimalsDeposit, SaberAddDecimalsWithdraw, TokenSwap, Sencha, Step, Cropper, Raydium, Lifinity,
|
|
Mercurial, Cykura, MarinadeDeposit, MarinadeUnstake, Meteora, GooseFX, Balansol, LifinityV2, RaydiumClmm,
|
|
TokenSwapV2, HeliumTreasuryManagementRedeemV0, StakeDexStakeWrappedSol, GooseFXV2, Perps, PerpsAddLiquidity,
|
|
PerpsRemoveLiquidity, MeteoraDlmm, RaydiumClmmV2, RaydiumCP, OneIntro, PumpWrappedBuy, PumpWrappedSell, PerpsV2,
|
|
PerpsV2AddLiquidity, PerpsV2RemoveLiquidity, MoonshotWrappedBuy, MoonshotWrappedSell, StabbleStableSwap,
|
|
StabbleWeightedSwap, FoxBuyFromEstimatedCost, SolayerDelegateNoInit, SolayerUndelegateNoInit, DaosFunBuy,
|
|
DaosFunSell, ZeroFi, StakeDexWithdrawWrappedSol, VirtualsBuy, VirtualsSell, PumpSwapBuy, PumpSwapSell,
|
|
Gamma, Woofi, MeteoraDammV2, MeteoraDynamicBondingCurveSwap, StabbleStableSwapV2, StabbleWeightedSwapV2,
|
|
BoopdotfunWrappedBuy, BoopdotfunWrappedSell, MeteoraDynamicBondingCurveSwapWithRemainingAccounts,
|
|
PumpWrappedBuyV2, PumpWrappedSellV2, PumpSwapBuyV2, PumpSwapSellV2, Aquifer, PumpWrappedBuyV3, PumpWrappedSellV3,
|
|
PumpSwapBuyV3, PumpSwapSellV3, JupiterLendDeposit, JupiterLendRedeem, RaydiumV2,
|
|
MeteoraDammV2WithRemainingAccounts, Obsidian, PumpWrappedBuyV4, PumpWrappedSellV4, CarrotIssue, CarrotRedeem:
|
|
return out, nil
|
|
|
|
// -------- bool payload --------
|
|
case Crema, Whirlpool, Invariant, DeltaFi, MarcoPolo, Obric, FoxClaimPartial, SolFi, Heaven, SolFiV2, AlphaQ,
|
|
SarosDlmm, BisonFi, PerenaStar, GoonFiV2:
|
|
return out, skipBool()
|
|
// -------- u32 --------
|
|
case StakeDexSwapViaStake, StakeDexPrefundWithdrawStakeAndDepositStake:
|
|
return out, skipU32()
|
|
// -------- u64 --------
|
|
case RaydiumLaunchlabBuy, RaydiumLaunchlabSell:
|
|
return out, skipU64()
|
|
// -------- Side(u8) payload --------
|
|
case Serum, Aldrin, AldrinV2, Dradex, Openbook, Phoenix, OpenBookV2, TokenMill, Plasma, TesseraV, Futarchy, WhaleStreet, Manifest:
|
|
return out, skipU8()
|
|
// -------- MeteoraDlmmSwapV2: RemainingAccountsInfo --------
|
|
case MeteoraDlmmSwapV2:
|
|
return out, skipRemaining()
|
|
// -------- DynamicV1: Vec<CandidateSwap> --------
|
|
case DynamicV1:
|
|
return out, skipDynamicV1(dec)
|
|
// -------- u64 + u64 --------
|
|
case Symmetry:
|
|
return out, skipTwoU64()
|
|
// -------- Clone: u8 + bool + bool --------
|
|
case Clone:
|
|
if err := skipU8(); err != nil {
|
|
return Swap{}, err
|
|
}
|
|
if err := skipBool(); err != nil {
|
|
return Swap{}, err
|
|
}
|
|
return out, skipBool()
|
|
// -------- SanctumS: u8 + u8 + u32 + u32 --------
|
|
case SanctumS:
|
|
if err := skipU8(); err != nil {
|
|
return Swap{}, err
|
|
}
|
|
if err := skipU8(); err != nil {
|
|
return Swap{}, err
|
|
}
|
|
if err := skipU32(); err != nil {
|
|
return Swap{}, err
|
|
}
|
|
return out, skipU32()
|
|
// -------- SanctumS(Add/Remove)Liquidity: u8 + u32 --------
|
|
case SanctumSAddLiquidity, SanctumSRemoveLiquidity:
|
|
if err := skipU8(); err != nil {
|
|
return Swap{}, err
|
|
}
|
|
return out, skipU32()
|
|
// -------- WhirlpoolSwapV2 / DefiTuna: bool + Option<RemainingAccountsInfo> --------
|
|
case WhirlpoolSwapV2, DefiTuna:
|
|
if err := skipBool(); err != nil {
|
|
return Swap{}, err
|
|
}
|
|
return out, skipOptRemaining()
|
|
// -------- Perena: u8 + u8 --------
|
|
case Perena:
|
|
if err := skipU8(); err != nil {
|
|
return Swap{}, err
|
|
}
|
|
return out, skipU8()
|
|
case GoonFi:
|
|
if err := skipBool(); err != nil {
|
|
return Swap{}, err
|
|
}
|
|
return out, skipU8()
|
|
// -------- HumidiFi/HumidiFiV2: u64 + bool --------
|
|
case HumidiFi, HumidiFiV2:
|
|
if err := skipU64(); err != nil {
|
|
return Swap{}, err
|
|
}
|
|
return out, skipBool()
|
|
|
|
// -------- JupiterRfqV2: Side(u8) + bytes --------
|
|
case JupiterRfqV2:
|
|
// side
|
|
if err := skipU8(); err != nil {
|
|
return Swap{}, err
|
|
}
|
|
ln, err := dec.ReadUint32(binary.LittleEndian)
|
|
if err != nil {
|
|
return Swap{}, err
|
|
}
|
|
if err := dec.SkipBytes(uint(ln)); err != nil {
|
|
return Swap{}, err
|
|
}
|
|
return out, nil
|
|
default:
|
|
// Unknown/new variant: assume no payload (keeps decoder aligned for RoutePlanStepV2 if it really is no-payload).
|
|
return out, nil
|
|
}
|
|
}
|
|
|
|
func decodeRoutePlanStepV2(dec *bin.Decoder) (RoutePlanStepV2, error) {
|
|
sw, err := decodeSwap(dec)
|
|
if err != nil {
|
|
return RoutePlanStepV2{}, err
|
|
}
|
|
bps, err := dec.ReadUint16(binary.LittleEndian)
|
|
if err != nil {
|
|
return RoutePlanStepV2{}, err
|
|
}
|
|
inIdx, err := dec.ReadUint8()
|
|
if err != nil {
|
|
return RoutePlanStepV2{}, err
|
|
}
|
|
outIdx, err := dec.ReadUint8()
|
|
if err != nil {
|
|
return RoutePlanStepV2{}, err
|
|
}
|
|
return RoutePlanStepV2{Swap: sw, Bps: bps, InputIdx: inIdx, OutputIdx: outIdx}, nil
|
|
}
|
|
|
|
func decodeRoutePlanStep(dec *bin.Decoder) (RoutePlanStep, error) {
|
|
sw, err := decodeSwap(dec)
|
|
if err != nil {
|
|
return RoutePlanStep{}, err
|
|
}
|
|
percent, err := dec.ReadUint8()
|
|
if err != nil {
|
|
return RoutePlanStep{}, err
|
|
}
|
|
inIdx, err := dec.ReadUint8()
|
|
if err != nil {
|
|
return RoutePlanStep{}, err
|
|
}
|
|
outIdx, err := dec.ReadUint8()
|
|
if err != nil {
|
|
return RoutePlanStep{}, err
|
|
}
|
|
return RoutePlanStep{Swap: sw, Percent: percent, InputIdx: inIdx, OutputIdx: outIdx}, nil
|
|
}
|
|
|
|
func decodeJupiterV6RouteV2Arg(data []byte) (*JupiterV6RouteV2Arg, error) {
|
|
dec := bin.NewBorshDecoder(data)
|
|
|
|
var err error
|
|
out := &JupiterV6RouteV2Arg{}
|
|
if out.In, err = dec.ReadUint64(binary.LittleEndian); err != nil {
|
|
return nil, err
|
|
}
|
|
if out.Out, err = dec.ReadUint64(binary.LittleEndian); err != nil {
|
|
return nil, err
|
|
}
|
|
if out.SlippageBps, err = dec.ReadUint16(binary.LittleEndian); err != nil {
|
|
return nil, err
|
|
}
|
|
if out.PlatformFeeBps, err = dec.ReadUint16(binary.LittleEndian); err != nil {
|
|
return nil, err
|
|
}
|
|
if out.PositiveSlippageBps, err = dec.ReadUint16(binary.LittleEndian); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// vec<RoutePlanStepV2>: u32 length + elements
|
|
ln, err := dec.ReadUint32(binary.LittleEndian)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out.Plan = make([]RoutePlanStepV2, 0, ln)
|
|
for i := uint32(0); i < ln; i++ {
|
|
step, err := decodeRoutePlanStepV2(dec)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("decode Plan[%d]: %w", i, err)
|
|
}
|
|
out.Plan = append(out.Plan, step)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
type JupiterV6RouteArg struct {
|
|
Plan []RoutePlanStep
|
|
|
|
In uint64
|
|
QuotedOut uint64
|
|
SlippageBps uint16
|
|
PlatformFeeBps uint8
|
|
}
|
|
|
|
type JupiterV6RouteWithTokenLedgerArg struct {
|
|
Plan []RoutePlanStep
|
|
|
|
QuotedOut uint64
|
|
SlippageBps uint16
|
|
PlatformFeeBps uint8
|
|
}
|
|
|
|
type JupiterV6SharedAccountsExactOutRouteArg struct {
|
|
ID uint8
|
|
|
|
Plan []RoutePlanStep
|
|
|
|
Out uint64
|
|
QuotedIn uint64
|
|
SlippageBps uint16
|
|
PlatformFeeBps uint8
|
|
}
|
|
|
|
type JupiterV6SharedAccountsRouteArg struct {
|
|
ID uint8
|
|
|
|
Plan []RoutePlanStep
|
|
|
|
In uint64
|
|
QuotedOut uint64
|
|
SlippageBps uint16
|
|
PlatformFeeBps uint8
|
|
}
|
|
|
|
type JupiterV6SharedAccountsRouteWithTokenLedgerArg struct {
|
|
ID uint8
|
|
|
|
Plan []RoutePlanStep
|
|
|
|
QuotedOut uint64
|
|
SlippageBps uint16
|
|
PlatformFeeBps uint8
|
|
}
|
|
|
|
type JupiterV6ExactOutRouteV2Arg struct {
|
|
Out uint64
|
|
QuotedIn uint64
|
|
Slippage uint16
|
|
PlatFee uint16
|
|
PosSlip uint16
|
|
RoutePlan []RoutePlanStepV2
|
|
}
|
|
|
|
type JupiterV6SharedAccountsExactOutRouteV2Arg struct {
|
|
ID uint8
|
|
|
|
Out uint64
|
|
QuotedIn uint64
|
|
Slippage uint16
|
|
PlatFee uint16
|
|
PosSlip uint16
|
|
RoutePlan []RoutePlanStepV2
|
|
}
|
|
|
|
type JupiterV6SharedAccountsRouteV2Arg struct {
|
|
ID uint8
|
|
|
|
In uint64
|
|
QuotedOut uint64
|
|
Slippage uint16
|
|
PlatFee uint16
|
|
PosSlip uint16
|
|
RoutePlan []RoutePlanStepV2
|
|
}
|
|
|
|
func decodeVecRoutePlanStep(dec *bin.Decoder) ([]RoutePlanStep, error) {
|
|
ln, err := dec.ReadUint32(binary.LittleEndian)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := make([]RoutePlanStep, 0, ln)
|
|
for i := uint32(0); i < ln; i++ {
|
|
step, err := decodeRoutePlanStep(dec)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("decode RoutePlanStep[%d]: %w", i, err)
|
|
}
|
|
out = append(out, step)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func decodeVecRoutePlanStepV2(dec *bin.Decoder) ([]RoutePlanStepV2, error) {
|
|
ln, err := dec.ReadUint32(binary.LittleEndian)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := make([]RoutePlanStepV2, 0, ln)
|
|
for i := uint32(0); i < ln; i++ {
|
|
step, err := decodeRoutePlanStepV2(dec)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("decode RoutePlanStepV2[%d]: %w", i, err)
|
|
}
|
|
out = append(out, step)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func decodeJupiterV6RouteArg(data []byte) (*JupiterV6RouteArg, error) {
|
|
dec := bin.NewBorshDecoder(data)
|
|
plan, err := decodeVecRoutePlanStep(dec)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
in, err := dec.ReadUint64(binary.LittleEndian)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
quotedOut, err := dec.ReadUint64(binary.LittleEndian)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
slippage, err := dec.ReadUint16(binary.LittleEndian)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pf, err := dec.ReadUint8()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &JupiterV6RouteArg{Plan: plan, In: in, QuotedOut: quotedOut, SlippageBps: slippage, PlatformFeeBps: pf}, nil
|
|
}
|
|
|
|
func decodeJupiterV6RouteWithTokenLedgerArg(data []byte) (*JupiterV6RouteWithTokenLedgerArg, error) {
|
|
dec := bin.NewBorshDecoder(data)
|
|
plan, err := decodeVecRoutePlanStep(dec)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
quotedOut, err := dec.ReadUint64(binary.LittleEndian)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
slippage, err := dec.ReadUint16(binary.LittleEndian)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pf, err := dec.ReadUint8()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &JupiterV6RouteWithTokenLedgerArg{Plan: plan, QuotedOut: quotedOut, SlippageBps: slippage, PlatformFeeBps: pf}, nil
|
|
}
|
|
|
|
func decodeJupiterV6SharedAccountsExactOutRouteArg(data []byte) (*JupiterV6SharedAccountsExactOutRouteArg, error) {
|
|
dec := bin.NewBorshDecoder(data)
|
|
id, err := dec.ReadUint8()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
plan, err := decodeVecRoutePlanStep(dec)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
outAmt, err := dec.ReadUint64(binary.LittleEndian)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
quotedIn, err := dec.ReadUint64(binary.LittleEndian)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
slippage, err := dec.ReadUint16(binary.LittleEndian)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pf, err := dec.ReadUint8()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &JupiterV6SharedAccountsExactOutRouteArg{ID: id, Plan: plan, Out: outAmt, QuotedIn: quotedIn, SlippageBps: slippage, PlatformFeeBps: pf}, nil
|
|
}
|
|
|
|
func decodeJupiterV6SharedAccountsRouteArg(data []byte) (*JupiterV6SharedAccountsRouteArg, error) {
|
|
dec := bin.NewBorshDecoder(data)
|
|
id, err := dec.ReadUint8()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
plan, err := decodeVecRoutePlanStep(dec)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
inAmt, err := dec.ReadUint64(binary.LittleEndian)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
quotedOut, err := dec.ReadUint64(binary.LittleEndian)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
slippage, err := dec.ReadUint16(binary.LittleEndian)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pf, err := dec.ReadUint8()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &JupiterV6SharedAccountsRouteArg{ID: id, Plan: plan, In: inAmt, QuotedOut: quotedOut, SlippageBps: slippage, PlatformFeeBps: pf}, nil
|
|
}
|
|
|
|
func decodeJupiterV6SharedAccountsRouteWithTokenLedgerArg(data []byte) (*JupiterV6SharedAccountsRouteWithTokenLedgerArg, error) {
|
|
dec := bin.NewBorshDecoder(data)
|
|
id, err := dec.ReadUint8()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
plan, err := decodeVecRoutePlanStep(dec)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
quotedOut, err := dec.ReadUint64(binary.LittleEndian)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
slippage, err := dec.ReadUint16(binary.LittleEndian)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pf, err := dec.ReadUint8()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &JupiterV6SharedAccountsRouteWithTokenLedgerArg{ID: id, Plan: plan, QuotedOut: quotedOut, SlippageBps: slippage, PlatformFeeBps: pf}, nil
|
|
}
|
|
|
|
func decodeJupiterV6ExactOutRouteV2Arg(data []byte) (*JupiterV6ExactOutRouteV2Arg, error) {
|
|
dec := bin.NewBorshDecoder(data)
|
|
outAmt, err := dec.ReadUint64(binary.LittleEndian)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
quotedIn, err := dec.ReadUint64(binary.LittleEndian)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
slippage, err := dec.ReadUint16(binary.LittleEndian)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pf, err := dec.ReadUint16(binary.LittleEndian)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pos, err := dec.ReadUint16(binary.LittleEndian)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
plan, err := decodeVecRoutePlanStepV2(dec)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &JupiterV6ExactOutRouteV2Arg{Out: outAmt, QuotedIn: quotedIn, Slippage: slippage, PlatFee: pf, PosSlip: pos, RoutePlan: plan}, nil
|
|
}
|
|
|
|
func decodeJupiterV6SharedAccountsExactOutRouteV2Arg(data []byte) (*JupiterV6SharedAccountsExactOutRouteV2Arg, error) {
|
|
dec := bin.NewBorshDecoder(data)
|
|
id, err := dec.ReadUint8()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
outAmt, err := dec.ReadUint64(binary.LittleEndian)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
quotedIn, err := dec.ReadUint64(binary.LittleEndian)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
slippage, err := dec.ReadUint16(binary.LittleEndian)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pf, err := dec.ReadUint16(binary.LittleEndian)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pos, err := dec.ReadUint16(binary.LittleEndian)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
plan, err := decodeVecRoutePlanStepV2(dec)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &JupiterV6SharedAccountsExactOutRouteV2Arg{ID: id, Out: outAmt, QuotedIn: quotedIn, Slippage: slippage, PlatFee: pf, PosSlip: pos, RoutePlan: plan}, nil
|
|
}
|
|
|
|
func decodeJupiterV6SharedAccountsRouteV2Arg(data []byte) (*JupiterV6SharedAccountsRouteV2Arg, error) {
|
|
dec := bin.NewBorshDecoder(data)
|
|
id, err := dec.ReadUint8()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
inAmt, err := dec.ReadUint64(binary.LittleEndian)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
quotedOut, err := dec.ReadUint64(binary.LittleEndian)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
slippage, err := dec.ReadUint16(binary.LittleEndian)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pf, err := dec.ReadUint16(binary.LittleEndian)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pos, err := dec.ReadUint16(binary.LittleEndian)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
plan, err := decodeVecRoutePlanStepV2(dec)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &JupiterV6SharedAccountsRouteV2Arg{ID: id, In: inAmt, QuotedOut: quotedOut, Slippage: slippage, PlatFee: pf, PosSlip: pos, RoutePlan: plan}, nil
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
return ret, i
|
|
}
|
|
|
|
func pumpSwapSellAtIdx0V2(amount uint64, plan []RoutePlanStepV2) (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.Bps) / 10000
|
|
}
|
|
}
|
|
return ret, i
|
|
}
|
|
|
|
type pumpWrappedMatch struct {
|
|
IsBuy bool
|
|
InAmount uint64
|
|
OutAmount uint64
|
|
}
|
|
|
|
func isPumpWrappedBuy(kind SwapKind) bool {
|
|
switch kind {
|
|
case PumpWrappedBuy, PumpWrappedBuyV2, PumpWrappedBuyV3, PumpWrappedBuyV4:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func isPumpWrappedSell(kind SwapKind) bool {
|
|
switch kind {
|
|
case PumpWrappedSell, PumpWrappedSellV2, PumpWrappedSellV3, PumpWrappedSellV4:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
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
|
|
count int
|
|
)
|
|
for _, step := range plan {
|
|
if step.InputIdx != 0 {
|
|
continue
|
|
}
|
|
if !isPumpWrappedBuy(step.Swap.Kind) && !isPumpWrappedSell(step.Swap.Kind) {
|
|
continue
|
|
}
|
|
count++
|
|
if count > 1 {
|
|
return pumpWrappedMatch{}, count
|
|
}
|
|
ret.IsBuy = isPumpWrappedBuy(step.Swap.Kind)
|
|
ret.InAmount = in * uint64(step.Percent) / 100
|
|
if step.Percent == 100 {
|
|
ret.OutAmount = out
|
|
}
|
|
}
|
|
return ret, count
|
|
}
|
|
|
|
func pumpWrappedAtIdx0V2(in uint64, out uint64, plan []RoutePlanStepV2) (pumpWrappedMatch, int) {
|
|
var (
|
|
ret pumpWrappedMatch
|
|
count int
|
|
)
|
|
for _, step := range plan {
|
|
if step.InputIdx != 0 {
|
|
continue
|
|
}
|
|
if !isPumpWrappedBuy(step.Swap.Kind) && !isPumpWrappedSell(step.Swap.Kind) {
|
|
continue
|
|
}
|
|
count++
|
|
if count > 1 {
|
|
return pumpWrappedMatch{}, count
|
|
}
|
|
ret.IsBuy = isPumpWrappedBuy(step.Swap.Kind)
|
|
ret.InAmount = in * uint64(step.Bps) / 10000
|
|
if step.Bps == 10000 {
|
|
ret.OutAmount = out
|
|
}
|
|
}
|
|
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))
|
|
if err != nil {
|
|
return solana.PublicKey{}, false, err
|
|
}
|
|
if !key.Equals(pumpProgramID) {
|
|
continue
|
|
}
|
|
if i+3 >= len(accounts) {
|
|
return solana.PublicKey{}, false, nil
|
|
}
|
|
mint, err := getStaticKey(staticKeys, int(accounts[i+3]))
|
|
if err != nil {
|
|
return solana.PublicKey{}, false, err
|
|
}
|
|
return mint, true, nil
|
|
}
|
|
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
|
|
if instructionIndex >= len(msg.Instructions) {
|
|
return nil, fmt.Errorf("instruction index out of bounds")
|
|
}
|
|
|
|
instruction := msg.Instructions[instructionIndex]
|
|
if len(instruction.Data) == 0 {
|
|
return nil, fmt.Errorf("data is empty")
|
|
}
|
|
if len(instruction.Data) < 8 {
|
|
return nil, nil
|
|
}
|
|
|
|
disc := instruction.Data[:8]
|
|
|
|
var (
|
|
sourceMint solana.PublicKey
|
|
inputAmount uint64
|
|
routeIn uint64
|
|
routeOut uint64
|
|
planCount int
|
|
wrapped pumpWrappedMatch
|
|
wrappedCnt int
|
|
wrappedAny pumpWrappedMatch
|
|
wrappedAnyC int
|
|
exactOut bool
|
|
err error
|
|
)
|
|
|
|
// route_v2 / exact_out_route_v2 / shared_accounts_*_v2 use accounts[3]/[4] as src/dst mints (per IDL)
|
|
// route/shared_accounts_* (v1) use different account layouts; we only decode args here.
|
|
switch {
|
|
case bytes.Equal(disc, jupiterRouteV2):
|
|
args, err := decodeJupiterV6RouteV2Arg(instruction.Data[8:])
|
|
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)
|
|
routeIn = args.In
|
|
routeOut = args.Out
|
|
case bytes.Equal(disc, jupiterSharedAccountsRouteV2):
|
|
args, err := decodeJupiterV6SharedAccountsRouteV2Arg(instruction.Data[8:])
|
|
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)
|
|
routeIn = args.In
|
|
routeOut = args.QuotedOut
|
|
case bytes.Equal(disc, jupiterExactOutRouteV2):
|
|
args, err := decodeJupiterV6ExactOutRouteV2Arg(instruction.Data[8:])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
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 {
|
|
return nil, err
|
|
}
|
|
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 {
|
|
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)
|
|
routeIn = args.In
|
|
routeOut = args.QuotedOut
|
|
case bytes.Equal(disc, jupiterSharedAccountsExactOutRoute):
|
|
args, err := decodeJupiterV6SharedAccountsExactOutRouteArg(instruction.Data[8:])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
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 {
|
|
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)
|
|
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)
|
|
}
|
|
if wrapped.InAmount > 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 wrapped.IsBuy {
|
|
event = "buy"
|
|
exactSol = !exactOut
|
|
token0AmountUint64 = wrapped.OutAmount
|
|
token1AmountUint64 = wrapped.InAmount
|
|
} else {
|
|
exactSol = exactOut && wrapped.OutAmount > 0
|
|
token0AmountUint64 = wrapped.InAmount
|
|
token1AmountUint64 = wrapped.OutAmount
|
|
}
|
|
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",
|
|
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",
|
|
Event: event,
|
|
IsToken2022: false,
|
|
IsMayhemMode: false,
|
|
ExactSOL: exactSol,
|
|
Block: tx.Block,
|
|
Token0AmountUint64: token0AmountUint64,
|
|
Token1AmountUint64: token1AmountUint64,
|
|
}, nil
|
|
}
|
|
if planCount > 1 {
|
|
// 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 {
|
|
return nil, nil
|
|
}
|
|
|
|
// 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 len(instruction.Accounts) < 6 {
|
|
return nil, fmt.Errorf("not enough accounts for jupiter v6 v2 instruction")
|
|
}
|
|
sourceMint, err = getStaticKey(tx.Message.StaticAccountKeys, int(instruction.Accounts[3]))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var (
|
|
srcIdx uint8
|
|
)
|
|
if len(instruction.Accounts) <= 9 {
|
|
return nil, nil
|
|
}
|
|
accounts := instruction.Accounts[8:]
|
|
for i, acctIdx := range accounts {
|
|
key, err := getStaticKey(tx.Message.StaticAccountKeys, int(acctIdx))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if key.Equals(pumpAmmProgramID) {
|
|
srcIdx = uint8(i + 4)
|
|
break
|
|
}
|
|
}
|
|
if srcIdx == 0 || srcIdx+1 >= uint8(len(accounts)) {
|
|
return nil, nil
|
|
}
|
|
|
|
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]))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !quoteMint.Equals(solana.WrappedSol) {
|
|
return nil, nil
|
|
}
|
|
|
|
} else if bytes.Equal(disc, jupiterSharedAccountsRoute) {
|
|
if len(instruction.Accounts) < 12 {
|
|
return nil, fmt.Errorf("not enough accounts for jupiter v6 jupiterSharedAccountsRoute instruction")
|
|
}
|
|
sourceMint, err = getStaticKey(tx.Message.StaticAccountKeys, int(instruction.Accounts[7]))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var (
|
|
srcIdx uint8
|
|
)
|
|
if len(instruction.Accounts) <= 12 {
|
|
return nil, nil
|
|
}
|
|
accounts := instruction.Accounts[11:]
|
|
for i, acctIdx := range accounts {
|
|
key, err := getStaticKey(tx.Message.StaticAccountKeys, int(acctIdx))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if key.Equals(pumpAmmProgramID) {
|
|
srcIdx = uint8(i + 4)
|
|
break
|
|
}
|
|
}
|
|
if srcIdx == 0 || srcIdx+1 >= uint8(len(accounts)) {
|
|
return nil, nil
|
|
}
|
|
|
|
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]))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !quoteMint.Equals(solana.WrappedSol) {
|
|
return nil, nil
|
|
}
|
|
} else {
|
|
if len(instruction.Accounts) < 10 {
|
|
return nil, fmt.Errorf("not enough accounts for jupiter v6 jupiterRoute instruction")
|
|
}
|
|
var (
|
|
srcIdx uint8
|
|
)
|
|
|
|
accounts := instruction.Accounts[9:]
|
|
for i, acctIdx := range accounts {
|
|
key, err := getStaticKey(tx.Message.StaticAccountKeys, int(acctIdx))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if key.Equals(pumpAmmProgramID) {
|
|
srcIdx = uint8(i + 4)
|
|
break
|
|
}
|
|
}
|
|
if srcIdx == 0 || srcIdx+1 >= uint8(len(accounts)) {
|
|
return nil, nil
|
|
}
|
|
sourceMint, err = getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx]))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
quoteMint, err := getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx+1]))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !quoteMint.Equals(solana.WrappedSol) {
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
signal := &TxSignal{
|
|
TxHash: tx.Signatures[0].String(),
|
|
Maker: tx.Message.StaticAccountKeys[0].String(),
|
|
Token0Address: sourceMint.String(),
|
|
Token1Address: wsolMint,
|
|
Token0Amount: formatTokenAmount(inputAmount),
|
|
Token1Amount: decimal.Zero,
|
|
Program: "PumpAMM",
|
|
Event: "sell",
|
|
IsToken2022: false,
|
|
IsMayhemMode: false,
|
|
ExactSOL: false,
|
|
Block: tx.Block,
|
|
Token0AmountUint64: inputAmount,
|
|
Token1AmountUint64: 0,
|
|
}
|
|
|
|
return signal, nil
|
|
}
|
|
|
|
// keep lints happy if solana-go isn't referenced elsewhere in this file for build tags
|
|
var _ = solana.PublicKey{}
|